Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219337ff78 | ||
|
|
f02609b3b6 |
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
target/
|
||||
console
|
||||
!console/
|
||||
portal-ui/node_modules/
|
||||
.git/
|
||||
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,49 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: community, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## NOTE
|
||||
|
||||
Please subscribe to our [paid subscription plans](https://min.io/pricing) for 24x7 support from our Engineering team.
|
||||
|
||||
<!--- Provide a general summary of the issue in the title above -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- If you're describing a bug, tell us what should happen -->
|
||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- or ideas how to implement the addition or change -->
|
||||
|
||||
## Steps to Reproduce (for bugs)
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
## Regression
|
||||
<!-- Is this issue a regression? (Yes / No) -->
|
||||
<!-- If Yes, optionally please include the MinIO version or commit id or PR# that caused this regression, if you have these details. -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* MinIO version used (`minio --version`):
|
||||
* Server setup and configuration:
|
||||
* Operating System and version (`uname -a`):
|
||||
34
.github/workflows/compiles.yml
vendored
Normal file
34
.github/workflows/compiles.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Compiles on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make console
|
||||
34
.github/workflows/crosscompile-1.yml
vendored
Normal file
34
.github/workflows/crosscompile-1.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'linux/ppc64le linux/mips64'"
|
||||
34
.github/workflows/crosscompile-2.yml
vendored
Normal file
34
.github/workflows/crosscompile-2.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'linux/arm64 linux/s390x'"
|
||||
34
.github/workflows/crosscompile-3.yml
vendored
Normal file
34
.github/workflows/crosscompile-3.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'darwin/amd64 freebsd/amd64'"
|
||||
34
.github/workflows/crosscompile-4.yml
vendored
Normal file
34
.github/workflows/crosscompile-4.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'windows/amd64 linux/arm'"
|
||||
34
.github/workflows/crosscompile-5.yml
vendored
Normal file
34
.github/workflows/crosscompile-5.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'linux/386 netbsd/amd64'"
|
||||
34
.github/workflows/go-test-pkg.yml
vendored
Normal file
34
.github/workflows/go-test-pkg.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test Pkg on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make test-pkg
|
||||
34
.github/workflows/go.yml
vendored
Normal file
34
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test Restapi on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make test
|
||||
18
.github/workflows/issues.yaml
vendored
18
.github/workflows/issues.yaml
vendored
@@ -1,18 +0,0 @@
|
||||
# @format
|
||||
|
||||
name: Issue Workflow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.5.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/miniohq/projects/2
|
||||
github-token: ${{ secrets.BOT_PAT }}
|
||||
1186
.github/workflows/jobs.yaml
vendored
1186
.github/workflows/jobs.yaml
vendored
File diff suppressed because it is too large
Load Diff
34
.github/workflows/lint.yml
vendored
Normal file
34
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Checking Lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make verifiers
|
||||
15
.github/workflows/react.yml
vendored
Normal file
15
.github/workflows/react.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: "React Tests"
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./portal-ui
|
||||
run: yarn
|
||||
- name: Run tests
|
||||
working-directory: ./portal-ui
|
||||
run: yarn test
|
||||
53
.github/workflows/vulncheck.yaml
vendored
53
.github/workflows/vulncheck.yaml
vendored
@@ -1,53 +0,0 @@
|
||||
# @format
|
||||
|
||||
name: Vulnerability Check
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
vulncheck:
|
||||
name: Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.8
|
||||
check-latest: true
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
shell: bash
|
||||
- name: Run govulncheck
|
||||
run: govulncheck ./...
|
||||
shell: bash
|
||||
|
||||
react-code-known-vulnerabilities:
|
||||
name: "React Code Has No Known Vulnerable Deps"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [ 1.23.x ]
|
||||
os: [ ubuntu-latest ]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Read .nvmrc
|
||||
id: node_version
|
||||
run: echo "$(cat .nvmrc)" && echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_ENV
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NVMRC }}
|
||||
- name: Checks for known security issues with the installed packages
|
||||
working-directory: ./web-app
|
||||
continue-on-error: false
|
||||
run: |
|
||||
yarn npm audit --recursive --environment production --no-deprecations
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,13 +1,3 @@
|
||||
# Playwright Data
|
||||
web-app/storage/
|
||||
web-app/playwright/.auth/admin.json
|
||||
|
||||
# Report from Playwright
|
||||
web-app/playwright-report/
|
||||
|
||||
# Coverage from Playwright
|
||||
web-app/.nyc_output/
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
@@ -29,7 +19,6 @@ vendor/
|
||||
|
||||
# Ignore executables
|
||||
target/
|
||||
!pkg/logger/target/
|
||||
console
|
||||
!console/
|
||||
|
||||
@@ -37,7 +26,7 @@ dist/
|
||||
|
||||
# Ignore node_modules
|
||||
|
||||
web-app/node_modules/
|
||||
portal-ui/node_modules/
|
||||
|
||||
# Ignore tls cert and key
|
||||
private.key
|
||||
@@ -48,6 +37,3 @@ public.crt
|
||||
*.code-workspace
|
||||
*~
|
||||
.eslintcache
|
||||
|
||||
# Ignore Bin files
|
||||
bin/
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
testifylint:
|
||||
disable:
|
||||
- go-require
|
||||
staticcheck:
|
||||
checks:
|
||||
[
|
||||
"all",
|
||||
"-ST1005",
|
||||
"-ST1000",
|
||||
"-SA4000",
|
||||
"-SA9004",
|
||||
"-SA1019",
|
||||
"-SA1008",
|
||||
"-U1000",
|
||||
"-ST1016",
|
||||
]
|
||||
goheader:
|
||||
values:
|
||||
regexp:
|
||||
copyright-holder: Copyright \(c\) (20\d\d\-20\d\d)|2021|({{year}})
|
||||
template-path: .license.tmpl
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- goimports
|
||||
- misspell
|
||||
- govet
|
||||
- revive
|
||||
- ineffassign
|
||||
- gosimple
|
||||
- gomodguard
|
||||
- gofmt
|
||||
- unused
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- durationcheck
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- should have a package comment
|
||||
# TODO(y4m4): Remove once all exported ident. have comments!
|
||||
- comment on exported function
|
||||
- comment on exported type
|
||||
- should have comment
|
||||
- use leading k in Go names
|
||||
- comment on exported const
|
||||
exclude-dirs:
|
||||
- api/operations
|
||||
105
.golangci.yml
105
.golangci.yml
@@ -1,73 +1,38 @@
|
||||
version: "2"
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
default: none
|
||||
disable-all: true
|
||||
enable:
|
||||
- durationcheck
|
||||
- gocritic
|
||||
- gomodguard
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
settings:
|
||||
goheader:
|
||||
values:
|
||||
regexp:
|
||||
copyright-holder: Copyright \(c\) (20\d\d\-20\d\d)|2021|({{year}})
|
||||
template-path: .license.tmpl
|
||||
misspell:
|
||||
locale: US
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -QF1001
|
||||
- -QF1008
|
||||
- -QF1010
|
||||
- -QF1012
|
||||
- -SA1008
|
||||
- -SA1019
|
||||
- -SA4000
|
||||
- -SA9004
|
||||
- -ST1000
|
||||
- -ST1005
|
||||
- -ST1016
|
||||
- -ST1019
|
||||
- -U1000
|
||||
testifylint:
|
||||
disable:
|
||||
- go-require
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- path: (.+)\.go$
|
||||
text: should have a package comment
|
||||
- path: (.+)\.go$
|
||||
text: comment on exported function
|
||||
- path: (.+)\.go$
|
||||
text: comment on exported type
|
||||
- path: (.+)\.go$
|
||||
text: should have comment
|
||||
- path: (.+)\.go$
|
||||
text: use leading k in Go names
|
||||
- path: (.+)\.go$
|
||||
text: comment on exported const
|
||||
paths:
|
||||
- api/operations
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- typecheck
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- api/operations
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- misspell
|
||||
- govet
|
||||
- revive
|
||||
- ineffassign
|
||||
- gosimple
|
||||
- deadcode
|
||||
- unparam
|
||||
- unused
|
||||
- structcheck
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.27.0 # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- should have a package comment
|
||||
# TODO(y4m4): Remove once all exported ident. have comments!
|
||||
- comment on exported function
|
||||
- comment on exported type
|
||||
- should have comment
|
||||
- use leading k in Go names
|
||||
- comment on exported const
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg/clientgen
|
||||
|
||||
195
.goreleaser.yml
Normal file
195
.goreleaser.yml
Normal file
@@ -0,0 +1,195 @@
|
||||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
project_name: console
|
||||
|
||||
release:
|
||||
name_template: "Release version {{.Tag}}"
|
||||
github:
|
||||
owner: minio
|
||||
name: console
|
||||
extra_files:
|
||||
- glob: "*.minisig"
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# you may remove this if you don't use vgo
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
-
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
main: ./cmd/console/
|
||||
|
||||
flags:
|
||||
- -trimpath
|
||||
- --tags=kqueue
|
||||
|
||||
ldflags:
|
||||
- -s -w -X github.com/minio/console/pkg.ReleaseTag={{.Tag}} -X github.com/minio/console/pkg.CommitID={{.FullCommit}} -X github.com/minio/console/pkg.Version={{.Version}} -X github.com/minio/console/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/console/pkg.ReleaseTime={{.Date}}
|
||||
|
||||
archives:
|
||||
-
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
||||
format: binary
|
||||
replacements:
|
||||
arm: arm
|
||||
|
||||
signs:
|
||||
-
|
||||
signature: "${artifact}.minisig"
|
||||
cmd: "sh"
|
||||
args:
|
||||
- '-c'
|
||||
- 'minisign -s /media/${USER}/minio/minisign.key -Sm ${artifact} < /media/${USER}/minio/minisign-passphrase'
|
||||
artifacts: all
|
||||
|
||||
snapshot:
|
||||
name_template: v0.0.0@{{.ShortCommit}}
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
||||
nfpms:
|
||||
-
|
||||
vendor: MinIO, Inc.
|
||||
homepage: https://github.com/minio/console
|
||||
maintainer: MinIO Development <dev@min.io>
|
||||
description: MinIO Console Server
|
||||
license: GNU Affero General Public License v3.0
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
contents:
|
||||
# Basic file that applies to all packagers
|
||||
- src: systemd/console.service
|
||||
dst: /etc/systemd/system/minio-console.service
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-amd64"
|
||||
use_buildx: true
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-ppc64le"
|
||||
use_buildx: true
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-s390x"
|
||||
use_buildx: true
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-arm64"
|
||||
use_buildx: true
|
||||
goarch: arm64
|
||||
goos: linux
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-amd64"
|
||||
use_buildx: true
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-ppc64le"
|
||||
use_buildx: true
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-s390x"
|
||||
use_buildx: true
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-arm64"
|
||||
use_buildx: true
|
||||
goarch: arm64
|
||||
goos: linux
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
docker_manifests:
|
||||
- name_template: minio/console:{{ .Tag }}
|
||||
image_templates:
|
||||
- minio/console:{{ .Tag }}-amd64
|
||||
- minio/console:{{ .Tag }}-arm64
|
||||
- minio/console:{{ .Tag }}-ppc64le
|
||||
- minio/console:{{ .Tag }}-s390x
|
||||
- name_template: quay.io/minio/console:{{ .Tag }}
|
||||
image_templates:
|
||||
- quay.io/minio/console:{{ .Tag }}-amd64
|
||||
- quay.io/minio/console:{{ .Tag }}-arm64
|
||||
- quay.io/minio/console:{{ .Tag }}-ppc64le
|
||||
- quay.io/minio/console:{{ .Tag }}-s390x
|
||||
- name_template: minio/console:latest
|
||||
image_templates:
|
||||
- minio/console:{{ .Tag }}-amd64
|
||||
- minio/console:{{ .Tag }}-arm64
|
||||
- minio/console:{{ .Tag }}-ppc64le
|
||||
- minio/console:{{ .Tag }}-s390x
|
||||
@@ -1,15 +0,0 @@
|
||||
This file is part of MinIO Console Server
|
||||
{{copyright-holder}} MinIO, Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Ignore git items
|
||||
.gitignore
|
||||
.git/
|
||||
:include .gitignore
|
||||
|
||||
# Common large paths
|
||||
node_modules/
|
||||
web-app/node_modules/
|
||||
build/
|
||||
dist/
|
||||
.idea/
|
||||
vendor/
|
||||
.env/
|
||||
.venv/
|
||||
.tox/
|
||||
*.min.js
|
||||
|
||||
# Common test paths
|
||||
test/
|
||||
tests/
|
||||
*_test.go
|
||||
|
||||
# Semgrep rules folder
|
||||
.semgrep
|
||||
|
||||
# Semgrep-action log folder
|
||||
.semgrep_logs/
|
||||
|
||||
# Ignore VsCode files
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
*~
|
||||
.eslintcache
|
||||
|
||||
consoleApi.ts
|
||||
463
CHANGELOG.md
463
CHANGELOG.md
@@ -1,463 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## Release v2.0.0
|
||||
|
||||
Community version is going back to be an object browser only.
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed Dependencies vulnerabilities
|
||||
|
||||
Deprecations:
|
||||
|
||||
- Deprecated support of accounts & policies management, this can be managed by using mc admin commands. Please refer to the [MinIO Console User Management page](https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/minio-user-management.html#id1) for more information.
|
||||
- Deprecated support of bucket management, this can be managed by using mc commands. Please refer to the [MinIO Client](https://min.io/docs/minio/linux/reference/minio-mc.html) for more information.
|
||||
- Deprecated support of configuration management, this can be managed by using mc admin config commands. Please refer to the [MinIO Client](https://min.io/docs/minio/linux/reference/minio-mc.html) for more information.
|
||||
|
||||
|
||||
## Release v1.7.6
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fix null pointer exception in Admin Info
|
||||
- Ignore leading or trailing spaces in login request
|
||||
- Fix file path on drag and drop
|
||||
- Fix typo in User DN Search Filter example
|
||||
|
||||
## Release v1.7.5
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed leaks during ZIP multiobject downloads
|
||||
- Allow spaces in Policy names
|
||||
|
||||
## Release v1.7.4
|
||||
|
||||
Deprecations:
|
||||
|
||||
- Deprecated support tools User Interface in favor of mc admin commands. Please refer to the [MinIO SUBNET Registration page](https://min.io/docs/minio/linux/administration/console/subnet-registration.html#subnet) for more information.
|
||||
- Deprecated Site replication User Interface in favor of mc admin commands. Please refer to the [MinIO Site Replication page](https://min.io/docs/minio/linux/operations/install-deploy-manage/multi-site-replication.html) for more information.
|
||||
- Deprecated Lifecycle & Tiers User Interface in favor of mc admin commands. Please refer to the [MinIO Tiers page](https://min.io/docs/minio/linux/reference/minio-mc/mc-ilm-tier.html) for more information.
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Avoid loading unpkg.com call when login animation is off
|
||||
|
||||
## Release v1.7.3
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Use a fixed public license verification key
|
||||
- Show non-expiring access keys as `no-expiry` instead of Jan 1, 1970
|
||||
- Use "join Slack" button for non-commercial edition instead of "Signup"
|
||||
- Fix setting policies on groups that have spaces
|
||||
|
||||
## Release v1.7.2
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed issue in Server Health Info
|
||||
- Fixed Security vulnerability in dependencies
|
||||
- Fixed client string in trace message
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Remove live logs in Call Home Page
|
||||
- Update License page
|
||||
|
||||
## Release v1.7.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed issue that could cause a failure when attempting to view deleted files in the object browser
|
||||
- Return network error when logging in and the network connection fails
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Added debug logging for console HTTP request (see [PR #3440](https://github.com/minio/console/pull/3440) for more detailed information)
|
||||
|
||||
## Release v1.7.0
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed directory listing
|
||||
- Fix MinIO videos link
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Removed deprecated KES functionality
|
||||
|
||||
## Release v1.6.3
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Updated go.mod version
|
||||
|
||||
## Release v1.6.2
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed minor user session issues
|
||||
- Updated project dependencies
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Improved Drives List visualization
|
||||
- Improved WS request logic
|
||||
- Updated License page with current MinIO plans.
|
||||
|
||||
## Release v1.6.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed objectManager issues under certain conditions
|
||||
- Fixed Security vulnerability in dependencies
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Improved Share Link behavior
|
||||
|
||||
## Release v1.6.0
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed share link encoding
|
||||
- Fixed Edit Lifecycle Storage Class
|
||||
- Added Tiers Improvements for Bucket Lifecycle management
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Vulnerability updates
|
||||
- Update Logo logic
|
||||
|
||||
## Release v1.5.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added remove Tier functionality
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed ILM rule tags not being shown
|
||||
- Fixed race condition Object Browser websocket
|
||||
- Fixed Encryption page crashing on empty response
|
||||
- Fixed Replication Delete Marker comparisons
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Use automatic URI encoding for APIs
|
||||
- Vulnerability updates
|
||||
|
||||
## Release v1.4.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added VersionID support to metadata details
|
||||
- Improved Websockets handlers
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerabilities and updated dependencies
|
||||
- Fixed an issue with Download URL decoding
|
||||
- Fixed leak in Object Browser Websocket
|
||||
- Minor UX fixes
|
||||
|
||||
## Release v1.3.0
|
||||
|
||||
Features:
|
||||
|
||||
- Adds ExpireDeleteMarker status to BucketLifecycleRule UI
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerability
|
||||
- Used URL-safe base64 enconding for Share API
|
||||
- Made Prefix field optional when Adding Tier
|
||||
- Added Console user agent in MinIO Admin Client
|
||||
|
||||
## Release v1.2.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated file share logic to work as Proxy
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Updated project dependencies
|
||||
- Fixed Key Permissions UX
|
||||
- Added permissions validation to rewind button
|
||||
- Fixed Health report upload to SUBNET
|
||||
- Misc Cosmetic fixes
|
||||
|
||||
## Release v1.1.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed folder download issue
|
||||
|
||||
## Release v1.1.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added Set Expired object all versions selector
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Updated Go Dependencies
|
||||
|
||||
## Release v1.0.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Preview message alert
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Updated Websocket API
|
||||
- Fixed issues with download manager
|
||||
- Fixed policies issues
|
||||
|
||||
## Release v0.46.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added latest help content to forms
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Disabled Create User button in certain policy cases
|
||||
- Fixed an issue with Logout request
|
||||
- Upgraded project dependencies
|
||||
|
||||
## Release v0.45.0
|
||||
|
||||
Deprecated:
|
||||
|
||||
- Deprecated Heal / Drives page
|
||||
|
||||
Features:
|
||||
|
||||
- Updated tines on menus & pages
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Upgraded project dependencies
|
||||
|
||||
## Release v0.44.0
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Upgraded project dependencies
|
||||
- Fixed events icons not loading in subpaths
|
||||
|
||||
## Release v0.43.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Update Share Object UI to reflect maximum expiration time in UI
|
||||
|
||||
## Release v0.43.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated PDF preview method
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerabilities
|
||||
- Prevented non-necessary metadata calls in object browser
|
||||
|
||||
## Release v0.42.2
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Hidden Prometheus metrics if URL is empty
|
||||
|
||||
## Release v0.42.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Reset go version to 1.19
|
||||
|
||||
## Release v0.42.0
|
||||
|
||||
Features:
|
||||
|
||||
- Introducing Dark Mode
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerabilities
|
||||
- Changes on Upload and Delete object urls
|
||||
- Fixed blocking subpath creation if not enough permissions
|
||||
- Removed share object option at prefix level
|
||||
- Updated allowed actions for a deleted object
|
||||
|
||||
## Release v0.41.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated pages to use mds components
|
||||
- support for resolving IPv4/IPv6
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Remove cache for ClientIP
|
||||
- Fixed override environment variables display in settings page
|
||||
- Fixed daylight savings time support in share modal
|
||||
|
||||
## Release v0.40.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated OpenID page
|
||||
- Added New bucket event types support
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed crash in access keys page
|
||||
- Fixed AuditLog filters issue
|
||||
- Fixed multiple issues with Object Browser
|
||||
|
||||
## Release v0.39.0
|
||||
|
||||
Features:
|
||||
|
||||
- Migrated metrics page to mds
|
||||
- Migrated Register page to mds
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed LDAP configuration page issues
|
||||
- Load available certificates in logout
|
||||
- Updated dependencies & go version
|
||||
- Fixed delete objects functionality
|
||||
|
||||
## Release v0.38.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added extra information to Service Accounts page
|
||||
- Updated Tiers, Site Replication, Speedtest, Heal & Watch pages components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed IDP expiry time errors
|
||||
- Updated project Dependencies
|
||||
|
||||
## Release v0.37.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Trace and Logs page components
|
||||
- Updated Prometheus metrics
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Disabled input fields for Subscription features if MinIO is not registered
|
||||
|
||||
## Release v0.36.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Settings page components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Show LDAP Enabled value LDAP configuration
|
||||
- Download multiple objects in same path as they were selected
|
||||
|
||||
## Release v0.35.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Change timestamp format for zip creation
|
||||
|
||||
## Release v0.35.0
|
||||
|
||||
Features:
|
||||
|
||||
- Add Exclude Folders and Exclude Prefixes during bucket creation
|
||||
- Download multiple selected objects as zip and ignore deleted objects
|
||||
- Updated Call Home, Inspet, Profile and Health components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Remove extra white spaces for configuration strings
|
||||
- Allow Create New Path in bucket view when having right permissions
|
||||
|
||||
## Release v0.34.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Buckets components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed SUBNET Health report upload
|
||||
- Updated Download Handler
|
||||
- Fixes issue with rewind
|
||||
- Avoid 1 hour expiration for IDP credentials
|
||||
|
||||
---
|
||||
|
||||
## Release v0.33.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated OpenID, LDAP components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed security issues
|
||||
- Fixed navigation issues in Object Browser
|
||||
- Fixed Dashboard metrics
|
||||
|
||||
---
|
||||
|
||||
## Release v0.32.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Users and Groups components
|
||||
- Added placeholder image for Help Menu
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed memory leak in WebSocket API for Object Browser
|
||||
|
||||
---
|
||||
|
||||
## Release v0.31.0
|
||||
|
||||
**Breaking Changes:**
|
||||
|
||||
- **Removed support for Standalone Deployments**
|
||||
|
||||
Features:
|
||||
|
||||
- Updated way files are displayed in uploading component
|
||||
- Updated Audit Logs and Policies components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed Download folders issue in Object Browser
|
||||
- Added missing Notification Events (ILM & REPLICA) in Events Notification Page
|
||||
- Fixed Security Vulnerability for `semver` dependency
|
||||
|
||||
---
|
||||
|
||||
## Release v0.30.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added MinIO Console Help Menu
|
||||
- Updated UI Menu components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Disable the Upload button on Object Browser if the user is not allowed
|
||||
- Fixed security vulnerability for `lestrrat-go/jwx` and `fast-xml-parser`
|
||||
- Fixed bug on sub-paths for Object Browser
|
||||
- Reduce the number of calls to `/session` API endpoint to improve performance
|
||||
- Rolled back the previous change for the Share File feature to no longer ask for Service Account access keys
|
||||
@@ -4,80 +4,56 @@ This is a REST portal server created using [go-swagger](https://github.com/go-sw
|
||||
|
||||
The API handlers are created using a YAML definition located in `swagger.YAML`.
|
||||
|
||||
To add new api, the YAML file needs to be updated with all the desired apis using
|
||||
the [Swagger Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/), this includes paths,
|
||||
parameters, definitions, tags, etc.
|
||||
To add new api, the YAML file needs to be updated with all the desired apis using the [Swagger Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/), this includes paths, parameters, definitions, tags, etc.
|
||||
|
||||
## Generate server from YAML
|
||||
|
||||
Once the YAML file is ready we can autogenerate the code needed for the new api by just running:
|
||||
|
||||
Validate it:
|
||||
|
||||
```
|
||||
swagger validate ./swagger.yml
|
||||
```
|
||||
|
||||
Update server code:
|
||||
|
||||
```
|
||||
make swagger-gen
|
||||
```
|
||||
|
||||
This will update all the necessary code.
|
||||
|
||||
`./api/configure_console.go` is a file that contains the handlers to be used by the application, here is the only place
|
||||
where we need to update our code to support the new apis. This file is not affected when running the swagger generator
|
||||
and it is safe to edit.
|
||||
`./restapi/configure_console.go` is a file that contains the handlers to be used by the application, here is the only place where we need to update our code to support the new apis. This file is not affected when running the swagger generator and it is safe to edit.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
`./api/handlers_test.go` needs to be updated with the proper tests for the new api.
|
||||
`./restapi/handlers_test.go` needs to be updated with the proper tests for the new api.
|
||||
|
||||
To run tests:
|
||||
|
||||
```
|
||||
go test ./api
|
||||
go test ./restapi
|
||||
```
|
||||
|
||||
## Commit changes
|
||||
|
||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to
|
||||
write useful commit messages
|
||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
||||
|
||||
```
|
||||
$ git commit -am 'Add some feature'
|
||||
```
|
||||
|
||||
### Push to the branch
|
||||
|
||||
Push your locally committed changes to the remote origin (your fork)
|
||||
|
||||
```
|
||||
$ git push origin my-new-feature
|
||||
```
|
||||
|
||||
### Create a Pull Request
|
||||
|
||||
Pull requests can be created via GitHub. Refer
|
||||
to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull
|
||||
request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||
|
||||
## FAQs
|
||||
|
||||
### How does ``console`` manages dependencies?
|
||||
|
||||
``MinIO`` uses `go mod` to manage its dependencies.
|
||||
|
||||
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
|
||||
|
||||
To remove a dependency
|
||||
|
||||
- Edit your code and remove the import reference.
|
||||
- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
|
||||
|
||||
### What are the coding guidelines for console?
|
||||
|
||||
``console`` is fully conformant with Golang style.
|
||||
Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe
|
||||
offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
|
||||
``console`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
|
||||
|
||||
@@ -1,82 +1,3 @@
|
||||
# Developing MinIO Console
|
||||
|
||||
The MinIO Console requires the [MinIO Server](https://github.com/minio/minio). For development purposes, you also need
|
||||
to run both the MinIO Console web app and the MinIO Console server.
|
||||
|
||||
## Running MinIO Console server
|
||||
|
||||
Build the server in the main folder by running:
|
||||
|
||||
```
|
||||
make
|
||||
```
|
||||
|
||||
> Note: If it's the first time running the server, you might need to run `go mod tidy` to ensure you have all modules
|
||||
> required.
|
||||
> To start the server run:
|
||||
|
||||
```
|
||||
CONSOLE_ACCESS_KEY=<your-access-key>
|
||||
CONSOLE_SECRET_KEY=<your-secret-key>
|
||||
CONSOLE_MINIO_SERVER=<minio-server-endpoint>
|
||||
CONSOLE_DEV_MODE=on
|
||||
./console server
|
||||
```
|
||||
|
||||
## Running MinIO Console web app
|
||||
|
||||
Refer to `/web-app` [instructions](/web-app/README.md) to run the web app locally.
|
||||
|
||||
# Building with MinIO
|
||||
|
||||
To test console in its shipping format, you need to build it from the MinIO repository, the following step will guide
|
||||
you to do that.
|
||||
|
||||
### 0. Building with UI Changes
|
||||
|
||||
If you are performing changes in the UI components of console and want to test inside the MinIO binary, you need to
|
||||
build assets first.
|
||||
|
||||
In the console folder run
|
||||
|
||||
```shell
|
||||
make assets
|
||||
```
|
||||
|
||||
This will regenerate all the static assets that will be served by MinIO.
|
||||
|
||||
### 1. Clone the `MinIO` repository
|
||||
|
||||
In the parent folder of where you cloned this `console` repository, clone the MinIO Repository
|
||||
|
||||
```shell
|
||||
git clone https://github.com/minio/minio.git
|
||||
```
|
||||
|
||||
### 2. Update `go.mod` to use your local version
|
||||
|
||||
In the MinIO repository open `go.mod` and after the first `require()` directive add a `replace()` directive
|
||||
|
||||
```
|
||||
...
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/minio/console => "../console"
|
||||
)
|
||||
|
||||
require (
|
||||
...
|
||||
```
|
||||
|
||||
### 3. Build `MinIO`
|
||||
|
||||
Still in the MinIO folder, run
|
||||
|
||||
```shell
|
||||
make build
|
||||
```
|
||||
|
||||
# LDAP authentication with Console
|
||||
|
||||
## Setup
|
||||
@@ -94,8 +15,7 @@ $ docker cp console/docs/ldap/billy.ldif my-openldap-container:/container/servic
|
||||
$ docker exec my-openldap-container ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin -f /container/service/slapd/assets/test/billy.ldif -H ldap://localhost
|
||||
```
|
||||
|
||||
Query the ldap server to check the user billy was created correctly and got assigned to the consoleAdmin group, you
|
||||
should get a list
|
||||
Query the ldap server to check the user billy was created correctly and got assigned to the consoleAdmin group, you should get a list
|
||||
containing ldap users and groups.
|
||||
|
||||
```
|
||||
@@ -110,7 +30,7 @@ $ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b uid=bil
|
||||
|
||||
### Change the password for user billy
|
||||
|
||||
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
|
||||
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
|
||||
|
||||
```
|
||||
$ docker exec -it my-openldap-container /bin/bash
|
||||
@@ -121,7 +41,6 @@ Enter LDAP Password:
|
||||
```
|
||||
|
||||
### Add the consoleAdmin policy to user billy on MinIO
|
||||
|
||||
```
|
||||
$ cat > consoleAdmin.json << EOF
|
||||
{
|
||||
@@ -147,8 +66,8 @@ $ cat > consoleAdmin.json << EOF
|
||||
]
|
||||
}
|
||||
EOF
|
||||
$ mc admin policy create myminio consoleAdmin consoleAdmin.json
|
||||
$ mc admin policy attach myminio consoleAdmin --user="uid=billy,dc=example,dc=org"
|
||||
$ mc admin policy add myminio consoleAdmin consoleAdmin.json
|
||||
$ mc admin policy set myminio consoleAdmin user="uid=billy,dc=example,dc=org"
|
||||
```
|
||||
|
||||
## Run MinIO
|
||||
|
||||
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
FROM node:10 as uilayer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./portal-ui/package.json ./
|
||||
COPY ./portal-ui/yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
COPY ./portal-ui .
|
||||
|
||||
RUN yarn install && make build-static
|
||||
|
||||
USER node
|
||||
|
||||
FROM golang:1.16 as golayer
|
||||
|
||||
RUN apt-get update -y && apt-get install -y ca-certificates
|
||||
|
||||
ADD go.mod /go/src/github.com/minio/console/go.mod
|
||||
ADD go.sum /go/src/github.com/minio/console/go.sum
|
||||
WORKDIR /go/src/github.com/minio/console/
|
||||
|
||||
# Get dependencies - will also be cached if we won't change mod/sum
|
||||
RUN go mod download
|
||||
|
||||
ADD . /go/src/github.com/minio/console/
|
||||
WORKDIR /go/src/github.com/minio/console/
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
COPY --from=uilayer /app/build /go/src/github.com/minio/console/portal-ui/build
|
||||
RUN go build -ldflags "-w -s" -a -o console ./cmd/console
|
||||
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
|
||||
MAINTAINER MinIO Development "dev@min.io"
|
||||
EXPOSE 9090
|
||||
|
||||
|
||||
COPY --from=golayer /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=golayer /go/src/github.com/minio/console/console .
|
||||
|
||||
ENTRYPOINT ["/console"]
|
||||
13
Dockerfile.assets
Normal file
13
Dockerfile.assets
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:10 as uilayer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./portal-ui/package.json ./
|
||||
COPY ./portal-ui/yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
COPY ./portal-ui .
|
||||
|
||||
RUN yarn install && make build-static
|
||||
|
||||
USER node
|
||||
23
Dockerfile.release
Normal file
23
Dockerfile.release
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
|
||||
|
||||
ARG TAG
|
||||
|
||||
COPY CREDITS /licenses/CREDITS
|
||||
COPY LICENSE /licenses/LICENSE
|
||||
|
||||
LABEL name="MinIO" \
|
||||
vendor="MinIO Inc <dev@min.io>" \
|
||||
maintainer="MinIO Inc <dev@min.io>" \
|
||||
version="${TAG}" \
|
||||
release="${TAG}" \
|
||||
summary="A graphical user interface for MinIO" \
|
||||
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
|
||||
|
||||
RUN \
|
||||
microdnf update --nodocs && \
|
||||
microdnf install ca-certificates --nodocs
|
||||
|
||||
EXPOSE 9090
|
||||
COPY console /console
|
||||
|
||||
ENTRYPOINT ["/console"]
|
||||
248
Makefile
248
Makefile
@@ -4,9 +4,6 @@ GOPATH := $(shell go env GOPATH)
|
||||
BUILD_VERSION:=$(shell git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
BUILD_TIME:=$(shell date 2>/dev/null)
|
||||
TAG ?= "minio/console:$(BUILD_VERSION)-dev"
|
||||
MINIO_VERSION ?= "quay.io/minio/minio:latest"
|
||||
TARGET_BUCKET ?= "target"
|
||||
NODE_VERSION := $(shell cat .nvmrc)
|
||||
|
||||
default: console
|
||||
|
||||
@@ -15,15 +12,23 @@ console:
|
||||
@echo "Building Console binary to './console'"
|
||||
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o console ./cmd/console)
|
||||
|
||||
k8sdev:
|
||||
@docker build -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' .
|
||||
@kind load docker-image $(TAG)
|
||||
@echo "Done, now restart your console deployment"
|
||||
|
||||
getdeps:
|
||||
@mkdir -p ${GOPATH}/bin
|
||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin
|
||||
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.40.1)
|
||||
|
||||
verifiers: getdeps fmt lint
|
||||
|
||||
fmt:
|
||||
@echo "Running $@ check"
|
||||
@(env bash $(PWD)/verify-gofmt.sh)
|
||||
@GO111MODULE=on gofmt -d restapi/
|
||||
@GO111MODULE=on gofmt -d pkg/
|
||||
@GO111MODULE=on gofmt -d cmd/
|
||||
@GO111MODULE=on gofmt -d cluster/
|
||||
|
||||
crosscompile:
|
||||
@(env bash $(PWD)/cross-compile.sh $(arg1))
|
||||
@@ -33,242 +38,39 @@ lint:
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
||||
|
||||
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml --fix
|
||||
|
||||
install: console
|
||||
@echo "Installing console binary to '$(GOPATH)/bin/console'"
|
||||
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/console $(GOPATH)/bin/console
|
||||
@echo "Installation successful. To learn more, try \"console --help\"."
|
||||
|
||||
swagger-gen: clean-swagger swagger-console apply-gofmt
|
||||
swagger-gen: clean-swagger swagger-console swagger-operator
|
||||
@echo "Done Generating swagger server code from yaml"
|
||||
|
||||
apply-gofmt:
|
||||
@echo "Applying gofmt to all generated an existing files"
|
||||
@GO111MODULE=on gofmt -w .
|
||||
|
||||
clean-swagger:
|
||||
@echo "cleaning"
|
||||
@rm -rf models
|
||||
@rm -rf api/operations
|
||||
@rm -rf restapi/operations
|
||||
@rm -rf operatorapi/operations
|
||||
|
||||
swagger-console:
|
||||
@echo "Generating swagger server code from yaml"
|
||||
@swagger generate server -A console --main-package=management --server-package=api --exclude-main -P models.Principal -f ./swagger.yml -r NOTICE
|
||||
@echo "Ensure basic install"
|
||||
@(cd web-app; yarn; cd ..)
|
||||
@echo "Generating typescript api"
|
||||
@make swagger-typescript-api path="../swagger.yml" output="./src/api" name="consoleApi.ts"
|
||||
@git restore api/server.go
|
||||
@swagger generate server -A console --main-package=management --server-package=restapi --exclude-main -P models.Principal -f ./swagger-console.yml -r NOTICE
|
||||
|
||||
swagger-typescript-api:
|
||||
@(cd web-app; yarn swagger-typescript-api -p $(path) -o $(output) -n $(name) --custom-config ../generator.config.js; cd ..)
|
||||
swagger-operator:
|
||||
@echo "Generating swagger server code from yaml"
|
||||
@swagger generate server -A operator --main-package=operator --server-package=operatorapi --exclude-main -P models.Principal -f ./swagger-operator.yml -r NOTICE
|
||||
|
||||
assets:
|
||||
@(if [ -f "${NVM_DIR}/nvm.sh" ]; then \. "${NVM_DIR}/nvm.sh" && nvm install && nvm use && npm install -g yarn ; fi &&\
|
||||
cd web-app; corepack enable; yarn install --prefer-offline; make build-static; yarn prettier --write . --log-level warn; cd ..)
|
||||
@(cd portal-ui; yarn install; make build-static; yarn prettier --write . --loglevel warn; cd ..)
|
||||
|
||||
test-integration:
|
||||
@(docker stop pgsqlcontainer || true)
|
||||
@(docker stop minio || true)
|
||||
@(docker stop minio2 || true)
|
||||
@(docker network rm mynet123 || true)
|
||||
@echo "create docker network to communicate containers MinIO & PostgreSQL"
|
||||
@(docker network create --subnet=173.18.0.0/29 mynet123)
|
||||
@echo "docker run with MinIO Version below:"
|
||||
@echo $(MINIO_VERSION)
|
||||
@echo "MinIO 1"
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 --net=mynet123 -d --name minio --rm -p 9000:9000 -p 9091:9091 -e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= $(MINIO_VERSION) server /data{1...4} --console-address ':9091' && sleep 5)
|
||||
@echo "MinIO 2"
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 --net=mynet123 -d --name minio2 --rm -p 9001:9001 -p 9092:9092 -e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= $(MINIO_VERSION) server /data{1...4} --address ':9001' --console-address ':9092' && sleep 5)
|
||||
@echo "Postgres"
|
||||
@(docker run --net=mynet123 --ip=173.18.0.4 --name pgsqlcontainer --rm -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres && sleep 5)
|
||||
@echo "execute test and get coverage for test-integration:"
|
||||
@(cd integration && go test -coverpkg=../api -c -tags testrunmain . && mkdir -p coverage && ./integration.test -test.v -test.run "^Test*" -test.coverprofile=coverage/system.out)
|
||||
@(docker stop pgsqlcontainer)
|
||||
@(docker stop minio)
|
||||
@(docker stop minio2)
|
||||
@(docker network rm mynet123)
|
||||
|
||||
test-replication:
|
||||
@(docker stop minio || true)
|
||||
@(docker stop minio1 || true)
|
||||
@(docker stop minio2 || true)
|
||||
@(docker network rm mynet123 || true)
|
||||
@(docker network create mynet123)
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--net=mynet123 -d \
|
||||
--name minio \
|
||||
--rm \
|
||||
-p 9000:9000 \
|
||||
-p 6000:6000 \
|
||||
-e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= \
|
||||
-e MINIO_ROOT_USER="minioadmin" \
|
||||
-e MINIO_ROOT_PASSWORD="minioadmin" \
|
||||
$(MINIO_VERSION) server /data{1...4} \
|
||||
--address :9000 \
|
||||
--console-address :6000)
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--net=mynet123 -d \
|
||||
--name minio1 \
|
||||
--rm \
|
||||
-p 9001:9001 \
|
||||
-p 6001:6001 \
|
||||
-e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= \
|
||||
-e MINIO_ROOT_USER="minioadmin" \
|
||||
-e MINIO_ROOT_PASSWORD="minioadmin" \
|
||||
$(MINIO_VERSION) server /data{1...4} \
|
||||
--address :9001 \
|
||||
--console-address :6001)
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--net=mynet123 -d \
|
||||
--name minio2 \
|
||||
--rm \
|
||||
-p 9002:9002 \
|
||||
-p 6002:6002 \
|
||||
-e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= \
|
||||
-e MINIO_ROOT_USER="minioadmin" \
|
||||
-e MINIO_ROOT_PASSWORD="minioadmin" \
|
||||
$(MINIO_VERSION) server /data{1...4} \
|
||||
--address :9002 \
|
||||
--console-address :6002)
|
||||
@(cd replication && go test -coverpkg=../api -c -tags testrunmain . && mkdir -p coverage && ./replication.test -test.v -test.run "^Test*" -test.coverprofile=coverage/replication.out)
|
||||
@(docker stop minio || true)
|
||||
@(docker stop minio1 || true)
|
||||
@(docker stop minio2 || true)
|
||||
@(docker network rm mynet123 || true)
|
||||
|
||||
test-sso-integration:
|
||||
@echo "create the network in bridge mode to communicate all containers"
|
||||
@(docker network create my-net)
|
||||
@echo "run openldap container using MinIO Image: quay.io/minio/openldap:latest"
|
||||
@(docker run \
|
||||
-e LDAP_ORGANIZATION="MinIO Inc" \
|
||||
-e LDAP_DOMAIN="min.io" \
|
||||
-e LDAP_ADMIN_PASSWORD="admin" \
|
||||
--network my-net \
|
||||
-p 389:389 \
|
||||
-p 636:636 \
|
||||
--name openldap \
|
||||
--detach quay.io/minio/openldap:latest)
|
||||
@echo "Run Dex container using MinIO Image: quay.io/minio/dex:latest"
|
||||
@(docker run \
|
||||
-e DEX_ISSUER=http://dex:5556/dex \
|
||||
-e DEX_CLIENT_REDIRECT_URI=http://127.0.0.1:9090/oauth_callback \
|
||||
-e DEX_LDAP_SERVER=openldap:389 \
|
||||
--network my-net \
|
||||
-p 5556:5556 \
|
||||
--name dex \
|
||||
--detach quay.io/minio/dex:latest)
|
||||
@echo "running minio server"
|
||||
@(docker run \
|
||||
-v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--network my-net \
|
||||
-d \
|
||||
--name minio \
|
||||
--rm \
|
||||
-p 9000:9000 \
|
||||
-p 9001:9001 \
|
||||
-e MINIO_IDENTITY_OPENID_CLIENT_ID="minio-client-app" \
|
||||
-e MINIO_IDENTITY_OPENID_CLIENT_SECRET="minio-client-app-secret" \
|
||||
-e MINIO_IDENTITY_OPENID_CLAIM_NAME=name \
|
||||
-e MINIO_IDENTITY_OPENID_CONFIG_URL=http://dex:5556/dex/.well-known/openid-configuration \
|
||||
-e MINIO_IDENTITY_OPENID_REDIRECT_URI=http://127.0.0.1:9090/oauth_callback \
|
||||
-e MINIO_ROOT_USER=minio \
|
||||
-e MINIO_ROOT_PASSWORD=minio123 $(MINIO_VERSION) server /data{1...4} --address :9000 --console-address :9001)
|
||||
@echo "run mc commands to set the policy"
|
||||
@(docker run --name minio-client --network my-net -dit --entrypoint=/bin/sh minio/mc)
|
||||
@(docker exec minio-client mc alias set myminio/ http://minio:9000 minio minio123)
|
||||
@echo "adding policy to Dillon Harper to be able to login:"
|
||||
@(cd sso-integration && docker cp allaccess.json minio-client:/ && docker exec minio-client mc admin policy create myminio "Dillon Harper" allaccess.json)
|
||||
@echo "starting bash script"
|
||||
@(env bash $(PWD)/sso-integration/set-sso.sh)
|
||||
@echo "add python module"
|
||||
@(pip3 install bs4)
|
||||
@echo "Executing the test:"
|
||||
@(cd sso-integration && go test -coverpkg=../api -c -tags testrunmain . && mkdir -p coverage && ./sso-integration.test -test.v -test.run "^Test*" -test.coverprofile=coverage/sso-system.out)
|
||||
|
||||
test-permissions-1:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-1/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-2:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-2/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-3:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-3/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-4:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-4/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-6:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-6/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-apply-permissions:
|
||||
@(env bash $(PWD)/web-app/tests/scripts/initialize-env.sh)
|
||||
|
||||
test-start-docker-minio:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
|
||||
initialize-permissions: test-start-docker-minio test-apply-permissions
|
||||
@echo "Done initializing permissions test"
|
||||
|
||||
cleanup-permissions:
|
||||
@(env bash $(PWD)/web-app/tests/scripts/cleanup-env.sh)
|
||||
@(docker stop minio)
|
||||
|
||||
initialize-docker-network:
|
||||
@(docker network create test-network)
|
||||
|
||||
test-start-docker-minio-w-redirect-url: initialize-docker-network
|
||||
@(docker run \
|
||||
-e MINIO_BROWSER_REDIRECT_URL='http://localhost:8000/console/subpath/' \
|
||||
-e MINIO_SERVER_URL='http://localhost:9000' \
|
||||
-v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
-d --network host --name minio --rm\
|
||||
quay.io/minio/minio:latest server /data{1...4})
|
||||
|
||||
test-start-docker-nginx-w-subpath:
|
||||
@(docker run \
|
||||
--network host \
|
||||
-d --rm \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-v ./web-app/tests/subpath-nginx/nginx.conf:/etc/nginx/nginx.conf \
|
||||
--name test-nginx nginx)
|
||||
|
||||
test-initialize-minio-nginx: test-start-docker-minio-w-redirect-url test-start-docker-nginx-w-subpath
|
||||
|
||||
cleanup-minio-nginx:
|
||||
@(docker stop minio test-nginx & docker network rm test-network)
|
||||
|
||||
# https://stackoverflow.com/questions/19200235/golang-tests-in-sub-directory
|
||||
# Note: go test ./... will run tests on the current folder and all subfolders.
|
||||
# This is needed because tests can be in the folder or sub-folder(s), let's include them all please!.
|
||||
test:
|
||||
@echo "execute test and get coverage"
|
||||
@(cd api && mkdir -p coverage && GO111MODULE=on go test ./... -test.v -coverprofile=coverage/coverage.out)
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/console/restapi/...)
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/19200235/golang-tests-in-sub-directory
|
||||
# Note: go test ./... will run tests on the current folder and all subfolders.
|
||||
# This is since tests in pkg folder are in subfolders and were not executed.
|
||||
test-pkg:
|
||||
@echo "execute test and get coverage"
|
||||
@(cd pkg && mkdir -p coverage && GO111MODULE=on go test ./... -test.v -coverprofile=coverage/coverage-pkg.out)
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/console/pkg/...)
|
||||
|
||||
coverage:
|
||||
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/console/api/... && go tool cover -html=coverage.out && open coverage.html)
|
||||
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/console/restapi/... && go tool cover -html=coverage.out && open coverage.html)
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files"
|
||||
@@ -277,10 +79,4 @@ clean:
|
||||
@rm -vf console
|
||||
|
||||
docker:
|
||||
@docker buildx build --output=type=docker --platform linux/amd64 -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' --build-arg NODE_VERSION='$(NODE_VERSION)' .
|
||||
|
||||
release: swagger-gen
|
||||
@echo "Generating Release: $(RELEASE)"
|
||||
@make assets
|
||||
@git add -u .
|
||||
@git add web-app/build/
|
||||
@docker build -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' .
|
||||
|
||||
2
NOTICE
2
NOTICE
@@ -1,5 +1,5 @@
|
||||
This file is part of MinIO Console Server
|
||||
Copyright (c) 2023 MinIO, Inc.
|
||||
Copyright (c) 2021 MinIO, Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
||||
163
README.md
163
README.md
@@ -4,28 +4,61 @@
|
||||
|
||||
A graphical user interface for [MinIO](https://github.com/minio/minio)
|
||||
|
||||
| Object Browser | Dashboard | Creating a bucket |
|
||||
|------------------------------------|-------------------------------|-------------------------------|
|
||||
|  |  |  |
|
||||
| Dashboard | Creating a bucket |
|
||||
| ------------- | ------------- |
|
||||
|  |  |
|
||||
|
||||
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
|
||||
**Table of Contents**
|
||||
|
||||
- [MinIO Console](#minio-console)
|
||||
- [Install](#install)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Setup](#setup)
|
||||
- [1. Create a user `console` using `mc`](#1-create-a-user-console-using-mc)
|
||||
- [2. Create a policy for `console` with admin access to all resources (for testing)](#2-create-a-policy-for-console-with-admin-access-to-all-resources-for-testing)
|
||||
- [3. Set the policy for the new `console` user](#3-set-the-policy-for-the-new-console-user)
|
||||
- [Start Console service:](#start-console-service)
|
||||
- [Start Console service with TLS:](#start-console-service-with-tls)
|
||||
- [Connect Console to a Minio using TLS and a self-signed certificate](#connect-console-to-a-minio-using-tls-and-a-self-signed-certificate)
|
||||
- [Install](#install)
|
||||
- [Binary Releases](#binary-releases)
|
||||
- [Docker](#docker)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Setup](#setup)
|
||||
- [1. Create a user `console` using `mc`](#1-create-a-user-console-using-mc)
|
||||
- [2. Create a policy for `console` with admin access to all resources (for testing)](#2-create-a-policy-for-console-with-admin-access-to-all-resources-for-testing)
|
||||
- [3. Set the policy for the new `console` user](#3-set-the-policy-for-the-new-console-user)
|
||||
- [Start Console service:](#start-console-service)
|
||||
- [Start Console service with TLS:](#start-console-service-with-tls)
|
||||
- [Connect Console to a Minio using TLS and a self-signed certificate](#connect-console-to-a-minio-using-tls-and-a-self-signed-certificate)
|
||||
- [Contribute to console Project](#contribute-to-console-project)
|
||||
|
||||
<!-- markdown-toc end -->
|
||||
|
||||
MinIO Console is a library that provides a management and browser UI overlay for the MinIO Server.
|
||||
## Install
|
||||
|
||||
### Binary Releases
|
||||
|
||||
| OS | ARCH | Binary |
|
||||
|:-------:|:-------:|:----------------------------------------------------------------------------------------------------:|
|
||||
| Linux | amd64 | [linux-amd64](https://github.com/minio/console/releases/latest/download/console-linux-amd64) |
|
||||
| Linux | arm64 | [linux-arm64](https://github.com/minio/console/releases/latest/download/console-linux-arm64) |
|
||||
| Linux | ppc64le | [linux-ppc64le](https://github.com/minio/console/releases/latest/download/console-linux-ppc64le) |
|
||||
| Linux | s390x | [linux-s390x](https://github.com/minio/console/releases/latest/download/console-linux-s390x) |
|
||||
| Apple | amd64 | [darwin-amd64](https://github.com/minio/console/releases/latest/download/console-darwin-amd64) |
|
||||
| Windows | amd64 | [windows-amd64](https://github.com/minio/console/releases/latest/download/console-windows-amd64.exe) |
|
||||
|
||||
You can also verify the binary with [minisign](https://jedisct1.github.io/minisign/) by downloading the corresponding [`.minisig`](https://github.com/minio/console/releases/latest) signature file. Then run:
|
||||
```
|
||||
minisign -Vm console-<OS>-<ARCH> -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Pull the latest release via:
|
||||
```
|
||||
docker pull minio/console
|
||||
```
|
||||
|
||||
### Build from source
|
||||
|
||||
```
|
||||
GO111MODULE=on go install github.com/minio/console/cmd/console@latest
|
||||
```
|
||||
> You will need a working Go environment. Therefore, please follow [How to install Go](https://golang.org/doc/install).
|
||||
> Minimum version required is go1.16
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -70,64 +103,60 @@ EOF
|
||||
```
|
||||
|
||||
```sh
|
||||
mc admin policy create myminio/ consoleAdmin admin.json
|
||||
mc admin policy add myminio/ consoleAdmin admin.json
|
||||
```
|
||||
|
||||
### 3. Set the policy for the new `console` user
|
||||
|
||||
```sh
|
||||
mc admin policy attach myminio consoleAdmin --user=console
|
||||
mc admin policy set myminio consoleAdmin user=console
|
||||
```
|
||||
|
||||
> NOTE: Additionally, you can create policies to limit the privileges for other `console` users, for example, if you
|
||||
> want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like
|
||||
> this:
|
||||
> NOTE: Additionally, you can create policies to limit the privileges for other `console` users, for example, if you want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"admin:ServerInfo"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListenBucketNotification",
|
||||
"s3:PutBucketNotification",
|
||||
"s3:GetBucketNotification",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:ListBucket",
|
||||
"s3:HeadBucket",
|
||||
"s3:GetObject",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:CreateBucket",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteBucket",
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:DeleteBucketPolicy",
|
||||
"s3:GetBucketPolicy"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Action": [
|
||||
"admin:ServerInfo"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListenBucketNotification",
|
||||
"s3:PutBucketNotification",
|
||||
"s3:GetBucketNotification",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:ListBucket",
|
||||
"s3:HeadBucket",
|
||||
"s3:GetObject",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:CreateBucket",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteBucket",
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:DeleteBucketPolicy",
|
||||
"s3:GetBucketPolicy"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Start Console service:
|
||||
|
||||
Before running console service, following environment settings must be supplied
|
||||
|
||||
```sh
|
||||
# Salt to encrypt JWT payload
|
||||
export CONSOLE_PBKDF_PASSPHRASE=SECRET
|
||||
@@ -140,7 +169,6 @@ export CONSOLE_MINIO_SERVER=http://localhost:9000
|
||||
```
|
||||
|
||||
Now start the console service.
|
||||
|
||||
```
|
||||
./console server
|
||||
2021-01-19 02:36:08.893735 I | 2021/01/19 02:36:08 server.go:129: Serving console at http://localhost:9090
|
||||
@@ -161,7 +189,6 @@ Copy your `public.crt` and `private.key` to `~/.console/certs`, then:
|
||||
For advanced users, `console` has support for multiple certificates to service clients through multiple domains.
|
||||
|
||||
Following tree structure is expected for supporting multiple domains:
|
||||
|
||||
```sh
|
||||
certs/
|
||||
│
|
||||
@@ -191,27 +218,5 @@ export CONSOLE_MINIO_SERVER=https://localhost:9000
|
||||
|
||||
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
|
||||
|
||||
## Debug logging
|
||||
|
||||
In some cases it may be convenient to log all HTTP requests. This can be enabled by setting
|
||||
the `CONSOLE_DEBUG_LOGLEVEL` environment variable to one of the following values:
|
||||
|
||||
- `0` (default) uses no logging.
|
||||
- `1` log single line per request for server-side errors (status-code 5xx).
|
||||
- `2` log single line per request for client-side and server-side errors (status-code 4xx/5xx).
|
||||
- `3` log single line per request for all requests (status-code 4xx/5xx).
|
||||
- `4` log details per request for server-side errors (status-code 5xx).
|
||||
- `5` log details per request for client-side and server-side errors (status-code 4xx/5xx).
|
||||
- `6` log details per request for all requests (status-code 4xx/5xx).
|
||||
|
||||
A single line logging has the following information:
|
||||
- Remote endpoint (IP + port) of the request. Note that reverse proxies may hide the actual remote endpoint of the client's browser.
|
||||
- HTTP method and URL
|
||||
- Status code of the response (websocket connections are hijacked, so no response is shown)
|
||||
- Duration of the request
|
||||
|
||||
The detailed logging also includes all request and response headers (if any).
|
||||
|
||||
# Contribute to console Project
|
||||
|
||||
Please follow console [Contributor's Guide](https://github.com/minio/console/blob/master/CONTRIBUTING.md)
|
||||
|
||||
@@ -18,10 +18,9 @@ you need access credentials for a successful exploit).
|
||||
|
||||
If you have not received a reply to your email within 48 hours or you have not heard from the security team
|
||||
for the past five days please contact the security team directly:
|
||||
|
||||
- Primary security coordinator: daniel@min.io
|
||||
- Secondary coordinator: security@min.io
|
||||
- If you receive no response: dev@min.io
|
||||
- Primary security coordinator: lenin@min.io
|
||||
- Secondary coordinator: security@min.io
|
||||
- If you receive no response: dev@min.io
|
||||
|
||||
### Disclosure Process
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
type AdminClientMock struct {
|
||||
minioAccountInfoMock func(ctx context.Context) (madmin.AccountInfo, error)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) kmsStatus(_ context.Context) (madmin.KMSStatus, error) {
|
||||
return madmin.KMSStatus{Name: "name", DefaultKeyID: "key", Endpoints: map[string]madmin.ItemState{"localhost": madmin.ItemState("online")}}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) AccountInfo(ctx context.Context) (madmin.AccountInfo, error) {
|
||||
return ac.minioAccountInfoMock(ctx)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/minio/mc/cmd"
|
||||
"github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
||||
type objectsListOpts struct {
|
||||
BucketName string
|
||||
Prefix string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
type ObjectsRequest struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
Prefix string `json:"prefix"`
|
||||
Date string `json:"date"`
|
||||
RequestID int64 `json:"request_id"`
|
||||
}
|
||||
|
||||
type WSResponse struct {
|
||||
RequestID int64 `json:"request_id,omitempty"`
|
||||
Error *CodedAPIError `json:"error,omitempty"`
|
||||
RequestEnd bool `json:"request_end,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
BucketName string `json:"bucketName,omitempty"`
|
||||
Data []ObjectResponse `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectResponse struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
LastModified string `json:"last_modified,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
VersionID string `json:"version_id,omitempty"`
|
||||
DeleteMarker bool `json:"delete_flag,omitempty"`
|
||||
IsLatest bool `json:"is_latest,omitempty"`
|
||||
}
|
||||
|
||||
func getObjectsOptionsFromReq(request ObjectsRequest) (*objectsListOpts, error) {
|
||||
pOptions := objectsListOpts{
|
||||
BucketName: request.BucketName,
|
||||
Prefix: request.Prefix,
|
||||
}
|
||||
|
||||
if request.Mode == "rewind" {
|
||||
parsedDate, errDate := time.Parse(time.RFC3339, request.Date)
|
||||
|
||||
if errDate != nil {
|
||||
return nil, errDate
|
||||
}
|
||||
|
||||
pOptions.Date = parsedDate
|
||||
}
|
||||
|
||||
return &pOptions, nil
|
||||
}
|
||||
|
||||
func startObjectsListing(ctx context.Context, client MinioClient, objOpts *objectsListOpts) <-chan minio.ObjectInfo {
|
||||
opts := minio.ListObjectsOptions{
|
||||
Prefix: objOpts.Prefix,
|
||||
}
|
||||
|
||||
return client.listObjects(ctx, objOpts.BucketName, opts)
|
||||
}
|
||||
|
||||
func startRewindListing(ctx context.Context, client MCClient, objOpts *objectsListOpts) <-chan *cmd.ClientContent {
|
||||
lsRewind := client.list(ctx, cmd.ListOptions{TimeRef: objOpts.Date, WithDeleteMarkers: true})
|
||||
|
||||
return lsRewind
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWSRewindObjects(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := s3ClientMock{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
testOptions objectsListOpts
|
||||
testMessages []*mc.ClientContent
|
||||
}{
|
||||
{
|
||||
name: "Get list with multiple elements",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/file1.txt"},
|
||||
},
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/file2.txt"},
|
||||
},
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/path1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list of elements",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "emptybucket",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{},
|
||||
},
|
||||
{
|
||||
name: "Get list with one element",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettestsingle",
|
||||
URL: mc.ClientURL{Path: "/file12.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get data from subpaths",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/path1/path2",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettestsingle",
|
||||
URL: mc.ClientURL{Path: "/path1/path2/file12.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
mcListMock = func(_ context.Context, _ mc.ListOptions) <-chan *mc.ClientContent {
|
||||
ch := make(chan *mc.ClientContent)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range tt.testMessages {
|
||||
ch <- m
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
rewindList := startRewindListing(ctx, client, &tt.testOptions)
|
||||
|
||||
// check that the rewindList got the same number of data from Console.
|
||||
|
||||
totalItems := 0
|
||||
for data := range rewindList {
|
||||
// Compare elements as we are defining the channel responses
|
||||
assert.Equal(tt.testMessages[totalItems].URL.Path, data.URL.Path)
|
||||
totalItems++
|
||||
}
|
||||
assert.Equal(len(tt.testMessages), totalItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSListObjects(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := minioClientMock{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
testOptions objectsListOpts
|
||||
testMessages []minio.ObjectInfo
|
||||
}{
|
||||
{
|
||||
name: "Get list with multiple elements",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/file1.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
{
|
||||
Key: "/file2.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
{
|
||||
Key: "/path1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list of elements",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "emptybucket",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{},
|
||||
},
|
||||
{
|
||||
name: "Get list with one element",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/file2.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get data from subpaths",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/path1/path2",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/path1/path2/file1.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
minioListObjectsMock = func(_ context.Context, _ string, _ minio.ListObjectsOptions) <-chan minio.ObjectInfo {
|
||||
ch := make(chan minio.ObjectInfo)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range tt.testMessages {
|
||||
ch <- m
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
objectsListing := startObjectsListing(ctx, client, &tt.testOptions)
|
||||
|
||||
// check that the TestReceiver got the same number of data from Console
|
||||
totalItems := 0
|
||||
for data := range objectsListing {
|
||||
// Compare elements as we are defining the channel responses
|
||||
assert.Equal(tt.testMessages[totalItems].Key, data.Key)
|
||||
totalItems++
|
||||
}
|
||||
assert.Equal(len(tt.testMessages), totalItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/console/pkg"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
const globalAppName = "MinIO Console"
|
||||
|
||||
// MinioAdmin interface with all functions to be implemented
|
||||
// by mock when testing, it should include all MinioAdmin respective api calls
|
||||
// that are used within this project.
|
||||
type MinioAdmin interface {
|
||||
AccountInfo(ctx context.Context) (madmin.AccountInfo, error)
|
||||
// KMS
|
||||
kmsStatus(ctx context.Context) (madmin.KMSStatus, error)
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
//
|
||||
// Define the structure of a minIO Client and define the functions that are actually used
|
||||
// from minIO api.
|
||||
type AdminClient struct {
|
||||
Client *madmin.AdminClient
|
||||
}
|
||||
|
||||
// AccountInfo implements madmin.AccountInfo()
|
||||
func (ac AdminClient) AccountInfo(ctx context.Context) (madmin.AccountInfo, error) {
|
||||
return ac.Client.AccountInfo(ctx, madmin.AccountOpts{})
|
||||
}
|
||||
|
||||
func (ac AdminClient) getBucketQuota(ctx context.Context, bucket string) (madmin.BucketQuota, error) {
|
||||
return ac.Client.GetBucketQuota(ctx, bucket)
|
||||
}
|
||||
|
||||
func (ac AdminClient) kmsStatus(ctx context.Context) (madmin.KMSStatus, error) {
|
||||
return ac.Client.KMSStatus(ctx)
|
||||
}
|
||||
|
||||
func NewMinioAdminClient(ctx context.Context, sessionClaims *models.Principal) (*madmin.AdminClient, error) {
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
adminClient, err := newAdminFromClaims(sessionClaims, clientIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient.SetAppInfo(globalAppName, pkg.Version)
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
|
||||
func newAdminFromClaims(claims *models.Principal, clientIP string) (*madmin.AdminClient, error) {
|
||||
tlsEnabled := getMinIOEndpointIsSecure()
|
||||
endpoint := getMinIOEndpoint()
|
||||
|
||||
adminClient, err := madmin.NewWithOptions(endpoint, &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken),
|
||||
Secure: tlsEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient.SetAppInfo(globalAppName, pkg.Version)
|
||||
adminClient.SetCustomTransport(PrepareSTSClientTransport(clientIP))
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
// isLocalAddress returns true if the url contains an IPv4/IPv6 hostname
|
||||
// that points to the local machine - FQDN are not supported
|
||||
func isLocalIPEndpoint(endpoint string) bool {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isLocalIPAddress(u.Hostname())
|
||||
}
|
||||
|
||||
// isLocalAddress returns true if the url contains an IPv4/IPv6 hostname
|
||||
// that points to the local machine - FQDN are not supported
|
||||
func isLocalIPAddress(ipAddr string) bool {
|
||||
if ipAddr == "" {
|
||||
return false
|
||||
}
|
||||
if ipAddr == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(ipAddr)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
// GetConsoleHTTPClient caches different http clients depending on the target endpoint while taking
|
||||
// in consideration CA certs stored in ${HOME}/.console/certs/CAs and ${HOME}/.minio/certs/CAs
|
||||
// If the target endpoint points to a loopback device, skip the TLS verification.
|
||||
func GetConsoleHTTPClient(clientIP string) *http.Client {
|
||||
return PrepareConsoleHTTPClient(clientIP)
|
||||
}
|
||||
|
||||
var (
|
||||
// De-facto standard header keys.
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
)
|
||||
|
||||
var (
|
||||
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||
// existing use of X-Forwarded-* headers.
|
||||
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||
forwarded = http.CanonicalHeaderKey("Forwarded")
|
||||
// Allows for a sub-match of the first value after 'for=' to the next
|
||||
// comma, semi-colon or space. The match is case-insensitive.
|
||||
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)(.*)`)
|
||||
)
|
||||
|
||||
// getSourceIPFromHeaders retrieves the IP from the X-Forwarded-For, X-Real-IP
|
||||
// and RFC7239 Forwarded headers (in that order)
|
||||
func getSourceIPFromHeaders(r *http.Request) string {
|
||||
var addr string
|
||||
|
||||
if fwd := r.Header.Get(xForwardedFor); fwd != "" {
|
||||
// Only grab the first (client) address. Note that '192.168.0.1,
|
||||
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
||||
// the first may represent forwarding proxies earlier in the chain.
|
||||
s := strings.Index(fwd, ", ")
|
||||
if s == -1 {
|
||||
s = len(fwd)
|
||||
}
|
||||
addr = fwd[:s]
|
||||
} else if fwd := r.Header.Get(xRealIP); fwd != "" {
|
||||
// X-Real-IP should only contain one IP address (the client making the
|
||||
// request).
|
||||
addr = fwd
|
||||
} else if fwd := r.Header.Get(forwarded); fwd != "" {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=' capture, which we ignore. In the case of multiple IP
|
||||
// addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only
|
||||
// extract the first, which should be the client IP.
|
||||
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
|
||||
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
||||
// these quotes.
|
||||
addr = strings.Trim(match[1], `"`)
|
||||
}
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// getClientIP retrieves the IP from the request headers
|
||||
// and falls back to r.RemoteAddr when necessary.
|
||||
// however returns without bracketing.
|
||||
func getClientIP(r *http.Request) string {
|
||||
addr := getSourceIPFromHeaders(r)
|
||||
if addr == "" {
|
||||
addr = r.RemoteAddr
|
||||
}
|
||||
|
||||
// Default to remote address if headers not set.
|
||||
raddr, _, _ := net.SplitHostPort(addr)
|
||||
if raddr == "" {
|
||||
return addr
|
||||
}
|
||||
return raddr
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2024 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_computeObjectURLWithoutEncode(t *testing.T) {
|
||||
type args struct {
|
||||
bucketName string
|
||||
prefix string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "http://localhost:9000/bucket-1/小飼弾小飼弾小飼弾.jp",
|
||||
args: args{
|
||||
bucketName: "bucket-1",
|
||||
prefix: "小飼弾小飼弾小飼弾.jpg",
|
||||
},
|
||||
want: "http://localhost:9000/bucket-1/小飼弾小飼弾小飼弾.jpg",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "http://localhost:9000/bucket-1/a a - a a & a a - a a a.jpg",
|
||||
args: args{
|
||||
bucketName: "bucket-1",
|
||||
prefix: "a a - a a & a a - a a a.jpg",
|
||||
},
|
||||
want: "http://localhost:9000/bucket-1/a a - a a & a a - a a a.jpg",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "http://localhost:9000/bucket-1/02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg",
|
||||
args: args{
|
||||
bucketName: "bucket-1",
|
||||
prefix: "02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg",
|
||||
},
|
||||
want: "http://localhost:9000/bucket-1/02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "http://localhost:9000/bucket-1/!@#$%^&*()_+.jpg",
|
||||
args: args{
|
||||
bucketName: "bucket-1",
|
||||
prefix: "!@#$%^&*()_+.jpg",
|
||||
},
|
||||
want: "http://localhost:9000/bucket-1/!@#$%^&*()_+.jpg",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "http://localhost:9000/bucket-1/test/test2/小飼弾小飼弾小飼弾.jpg",
|
||||
args: args{
|
||||
bucketName: "bucket-1",
|
||||
prefix: "test/test2/小飼弾小飼弾小飼弾.jpg",
|
||||
},
|
||||
want: "http://localhost:9000/bucket-1/test/test2/小飼弾小飼弾小飼弾.jpg",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got, err := computeObjectURLWithoutEncode(tt.args.bucketName, tt.args.prefix)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("computeObjectURLWithoutEncode() errors = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err == nil {
|
||||
if got != tt.want {
|
||||
t.Errorf("computeObjectURLWithoutEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetHostname(t *testing.T) {
|
||||
os.Setenv(ConsoleHostname, "x")
|
||||
defer os.Unsetenv(ConsoleHostname)
|
||||
assert.Equalf(t, "x", GetHostname(), "GetHostname()")
|
||||
}
|
||||
|
||||
func TestGetPort(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "valid port",
|
||||
args: args{
|
||||
env: "9091",
|
||||
},
|
||||
want: 9091,
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 9090,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsolePort, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetPort(), "GetPort()")
|
||||
os.Unsetenv(ConsolePort)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTLSPort(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "valid port",
|
||||
args: args{
|
||||
env: "9444",
|
||||
},
|
||||
want: 9444,
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 9443,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleTLSPort, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetTLSPort(), "GetTLSPort()")
|
||||
os.Unsetenv(ConsoleTLSPort)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecureAllowedHosts(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid hosts",
|
||||
args: args{
|
||||
env: "host1,host2",
|
||||
},
|
||||
want: []string{"host1", "host2"},
|
||||
},
|
||||
{
|
||||
name: "empty hosts",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleSecureAllowedHosts, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetSecureAllowedHosts(), "GetSecureAllowedHosts()")
|
||||
os.Unsetenv(ConsoleSecureAllowedHosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecureHostsProxyHeaders(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid headers",
|
||||
args: args{
|
||||
env: "header1,header2",
|
||||
},
|
||||
want: []string{"header1", "header2"},
|
||||
},
|
||||
{
|
||||
name: "empty headers",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleSecureHostsProxyHeaders, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetSecureHostsProxyHeaders(), "GetSecureHostsProxyHeaders()")
|
||||
os.Unsetenv(ConsoleSecureHostsProxyHeaders)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecureSTSSeconds(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
env: "1",
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleSecureSTSSeconds, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetSecureSTSSeconds(), "GetSecureSTSSeconds()")
|
||||
os.Unsetenv(ConsoleSecureSTSSeconds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLogSearchAPIToken(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env set",
|
||||
args: args{
|
||||
env: "value",
|
||||
},
|
||||
want: "value",
|
||||
},
|
||||
{
|
||||
name: "env not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleLogQueryAuthToken, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getLogSearchAPIToken(), "getLogSearchAPIToken()")
|
||||
os.Setenv(ConsoleLogQueryAuthToken, tt.args.env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getPrometheusURL(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env set",
|
||||
args: args{
|
||||
env: "value",
|
||||
},
|
||||
want: "value",
|
||||
},
|
||||
{
|
||||
name: "env not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(PrometheusURL, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getPrometheusURL(), "getPrometheusURL()")
|
||||
os.Setenv(PrometheusURL, tt.args.env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getPrometheusJobID(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env set",
|
||||
args: args{
|
||||
env: "value",
|
||||
},
|
||||
want: "value",
|
||||
},
|
||||
{
|
||||
name: "env not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: "minio-job",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(PrometheusJobID, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getPrometheusJobID(), "getPrometheusJobID()")
|
||||
os.Setenv(PrometheusJobID, tt.args.env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getMaxConcurrentUploadsLimit(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
env: "1",
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 10,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleMaxConcurrentUploads, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getMaxConcurrentUploadsLimit(), "getMaxConcurrentUploadsLimit()")
|
||||
os.Unsetenv(ConsoleMaxConcurrentUploads)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getMaxConcurrentDownloadsLimit(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
env: "1",
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 20,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleMaxConcurrentDownloads, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getMaxConcurrentDownloadsLimit(), "getMaxConcurrentDownloadsLimit()")
|
||||
os.Unsetenv(ConsoleMaxConcurrentDownloads)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getConsoleDevMode(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "value set",
|
||||
args: args{
|
||||
env: "on",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "value not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleDevMode, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getConsoleDevMode(), "getConsoleDevMode()")
|
||||
os.Unsetenv(ConsoleDevMode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// This file is safe to edit. Once it exists it will not be overwritten
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
"github.com/minio/console/pkg/utils"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
|
||||
portal_ui "github.com/minio/console/web-app"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
"github.com/minio/pkg/v3/mimedb"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth"
|
||||
"github.com/unrolled/secure"
|
||||
)
|
||||
|
||||
//go:generate swagger generate server --target ../../console --name Console --spec ../swagger.yml
|
||||
|
||||
var additionalServerFlags = struct {
|
||||
CertsDir string `long:"certs-dir" description:"path to certs directory" env:"CONSOLE_CERTS_DIR"`
|
||||
}{}
|
||||
|
||||
const (
|
||||
SubPath = "CONSOLE_SUBPATH"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgSubPath = "/"
|
||||
subPathOnce sync.Once
|
||||
)
|
||||
|
||||
func configureFlags(api *operations.ConsoleAPI) {
|
||||
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
|
||||
{
|
||||
ShortDescription: "additional server flags",
|
||||
Options: &additionalServerFlags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
||||
// Applies when the "x-token" header is set
|
||||
api.KeyAuth = func(token string, _ []string) (*models.Principal, error) {
|
||||
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
|
||||
// was generated and signed by us in the first place
|
||||
if token == "Anonymous" {
|
||||
return &models.Principal{}, nil
|
||||
}
|
||||
claims, err := auth.ParseClaimsFromToken(token)
|
||||
if err != nil {
|
||||
api.Logger("Unable to validate the session token %s: %v", token, err)
|
||||
return nil, errors.New(401, "incorrect api key auth")
|
||||
}
|
||||
return &models.Principal{
|
||||
STSAccessKeyID: claims.STSAccessKeyID,
|
||||
STSSecretAccessKey: claims.STSSecretAccessKey,
|
||||
STSSessionToken: claims.STSSessionToken,
|
||||
AccountAccessKey: claims.AccountAccessKey,
|
||||
Hm: claims.HideMenu,
|
||||
Ob: claims.ObjectBrowser,
|
||||
CustomStyleOb: claims.CustomStyleOB,
|
||||
}, nil
|
||||
}
|
||||
api.AnonymousAuth = func(_ string) (*models.Principal, error) {
|
||||
return &models.Principal{}, nil
|
||||
}
|
||||
|
||||
// Register login handlers
|
||||
registerLoginHandlers(api)
|
||||
// Register logout handlers
|
||||
registerLogoutHandlers(api)
|
||||
// Register bucket handlers
|
||||
registerBucketsHandlers(api)
|
||||
// Register session handlers
|
||||
registerSessionHandlers(api)
|
||||
// Register Object's Handlers
|
||||
registerObjectsHandlers(api)
|
||||
// Register Bucket Quota's Handlers
|
||||
registerBucketQuotaHandlers(api)
|
||||
// Register Bucket Policy's Handlers
|
||||
registerPublicObjectsHandlers(api)
|
||||
|
||||
api.PreServerShutdown = func() {}
|
||||
|
||||
api.ServerShutdown = func() {}
|
||||
|
||||
return setupGlobalMiddleware(api.Serve(setupMiddlewares))
|
||||
}
|
||||
|
||||
// The TLS configuration before HTTPS server starts.
|
||||
func configureTLS(tlsConfig *tls.Config) {
|
||||
tlsConfig.RootCAs = GlobalRootCAs
|
||||
tlsConfig.GetCertificate = GlobalTLSCertsManager.GetCertificate
|
||||
}
|
||||
|
||||
// The middleware configuration is for the handler executors. These do not apply to the swagger.json document.
|
||||
// The middleware executes after routing but before authentication, binding and validation
|
||||
func setupMiddlewares(handler http.Handler) http.Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
func ContextMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := uuid.NewString()
|
||||
ctx := context.WithValue(r.Context(), utils.ContextRequestID, requestID)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, r.UserAgent())
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestHost, r.Host)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, r.RemoteAddr)
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(r))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func AuditLogMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rw := logger.NewResponseWriter(w)
|
||||
next.ServeHTTP(rw, r)
|
||||
if strings.HasPrefix(r.URL.Path, "/ws") || strings.HasPrefix(r.URL.Path, "/api") {
|
||||
logger.AuditLog(r.Context(), rw, r, map[string]interface{}{}, "Authorization", "Cookie", "Set-Cookie")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func DebugLogMiddleware(next http.Handler) http.Handler {
|
||||
debugLogLevel, _ := env.GetInt("CONSOLE_DEBUG_LOGLEVEL", 0)
|
||||
if debugLogLevel == 0 {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rw := logger.NewResponseWriter(w)
|
||||
next.ServeHTTP(rw, r)
|
||||
debugLog(debugLogLevel, r, rw)
|
||||
})
|
||||
}
|
||||
|
||||
func debugLog(debugLogLevel int, r *http.Request, rw *logger.ResponseWriter) {
|
||||
switch debugLogLevel {
|
||||
case 1:
|
||||
// Log server errors only (summary)
|
||||
if rw.StatusCode >= 500 {
|
||||
debugLogSummary(r, rw)
|
||||
}
|
||||
case 2:
|
||||
// Log server and client errors (summary)
|
||||
if rw.StatusCode >= 400 {
|
||||
debugLogSummary(r, rw)
|
||||
}
|
||||
case 3:
|
||||
// Log all requests (summary)
|
||||
debugLogSummary(r, rw)
|
||||
case 4:
|
||||
// Log server errors only (including headers)
|
||||
if rw.StatusCode >= 500 {
|
||||
debugLogDetails(r, rw)
|
||||
}
|
||||
case 5:
|
||||
// Log server and client errors (including headers)
|
||||
if rw.StatusCode >= 400 {
|
||||
debugLogDetails(r, rw)
|
||||
}
|
||||
case 6:
|
||||
// Log all requests (including headers)
|
||||
debugLogDetails(r, rw)
|
||||
}
|
||||
}
|
||||
|
||||
func debugLogSummary(r *http.Request, rw *logger.ResponseWriter) {
|
||||
statusCode := strconv.Itoa(rw.StatusCode)
|
||||
if rw.Hijacked {
|
||||
statusCode = "hijacked"
|
||||
}
|
||||
logger.Info(fmt.Sprintf("%s %s %s %s %dms", r.RemoteAddr, r.Method, r.URL, statusCode, time.Since(rw.StartTime).Milliseconds()))
|
||||
}
|
||||
|
||||
func debugLogDetails(r *http.Request, rw *logger.ResponseWriter) {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("- Method/URL: %s %s\n", r.Method, r.URL))
|
||||
sb.WriteString(fmt.Sprintf(" Remote endpoint: %s\n", r.RemoteAddr))
|
||||
if rw.Hijacked {
|
||||
sb.WriteString(" Status code: <hijacked, probably a websocket>\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" Status code: %d\n", rw.StatusCode))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" Duration (ms): %d\n", time.Since(rw.StartTime).Milliseconds()))
|
||||
sb.WriteString(" Request headers: ")
|
||||
debugLogHeaders(&sb, r.Header)
|
||||
sb.WriteString(" Response headers: ")
|
||||
debugLogHeaders(&sb, rw.Header())
|
||||
logger.Info(sb.String())
|
||||
}
|
||||
|
||||
func debugLogHeaders(sb *strings.Builder, h http.Header) {
|
||||
keys := make([]string, 0, len(h))
|
||||
for key := range h {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
first := true
|
||||
for _, key := range keys {
|
||||
values := h[key]
|
||||
for _, value := range values {
|
||||
if !first {
|
||||
sb.WriteString(" ")
|
||||
} else {
|
||||
first = false
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
||||
}
|
||||
}
|
||||
if first {
|
||||
sb.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
|
||||
// So this is a good place to plug in a panic handling middleware, logger and metrics
|
||||
func setupGlobalMiddleware(handler http.Handler) http.Handler {
|
||||
gnext := gzhttp.GzipHandler(handler)
|
||||
// if audit-log is enabled console will log all incoming request
|
||||
next := AuditLogMiddleware(gnext)
|
||||
// serve static files
|
||||
next = FileServerMiddleware(next)
|
||||
// add information to request context
|
||||
next = ContextMiddleware(next)
|
||||
// handle cookie or authorization header for session
|
||||
next = AuthenticationMiddleware(next)
|
||||
// handle debug logging
|
||||
next = DebugLogMiddleware(next)
|
||||
|
||||
sslHostFn := secure.SSLHostFunc(func(host string) string {
|
||||
xhost, err := xnet.ParseHost(host)
|
||||
if err != nil {
|
||||
return host
|
||||
}
|
||||
return net.JoinHostPort(xhost.Name, TLSPort)
|
||||
})
|
||||
|
||||
// Secure middleware, this middleware wrap all the previous handlers and add
|
||||
// HTTP security headers
|
||||
secureOptions := secure.Options{
|
||||
AllowedHosts: GetSecureAllowedHosts(),
|
||||
AllowedHostsAreRegex: GetSecureAllowedHostsAreRegex(),
|
||||
HostsProxyHeaders: GetSecureHostsProxyHeaders(),
|
||||
SSLRedirect: GetTLSRedirect() == "on" && len(GlobalPublicCerts) > 0,
|
||||
SSLHostFunc: &sslHostFn,
|
||||
SSLHost: GetSecureTLSHost(),
|
||||
STSSeconds: GetSecureSTSSeconds(),
|
||||
STSIncludeSubdomains: GetSecureSTSIncludeSubdomains(),
|
||||
STSPreload: GetSecureSTSPreload(),
|
||||
SSLTemporaryRedirect: false,
|
||||
ForceSTSHeader: GetSecureForceSTSHeader(),
|
||||
FrameDeny: GetSecureFrameDeny(),
|
||||
ContentTypeNosniff: GetSecureContentTypeNonSniff(),
|
||||
BrowserXssFilter: GetSecureBrowserXSSFilter(),
|
||||
ContentSecurityPolicy: GetSecureContentSecurityPolicy(),
|
||||
ContentSecurityPolicyReportOnly: GetSecureContentSecurityPolicyReportOnly(),
|
||||
ReferrerPolicy: GetSecureReferrerPolicy(),
|
||||
FeaturePolicy: GetSecureFeaturePolicy(),
|
||||
IsDevelopment: false,
|
||||
}
|
||||
secureMiddleware := secure.New(secureOptions)
|
||||
next = secureMiddleware.Handler(next)
|
||||
return RejectS3Middleware(next)
|
||||
}
|
||||
|
||||
const apiRequestErr = `<?xml version="1.0" encoding="UTF-8"?><Error><Code>InvalidArgument</Code><Message>S3 API Requests must be made to API port.</Message><RequestId>0</RequestId></Error>`
|
||||
|
||||
// RejectS3Middleware will reject requests that have AWS S3 specific headers.
|
||||
func RejectS3Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(r.Header.Get("X-Amz-Content-Sha256")) > 0 ||
|
||||
len(r.Header.Get("X-Amz-Date")) > 0 ||
|
||||
strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256") ||
|
||||
r.URL.Query().Get("AWSAccessKeyId") != "" {
|
||||
|
||||
w.Header().Set("Location", getMinIOServer())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(apiRequestErr))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := auth.GetTokenFromRequest(r)
|
||||
if err != nil && err != auth.ErrNoAuthToken {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sessionToken, _ := auth.DecryptToken(token)
|
||||
// All handlers handle appropriately to return errors
|
||||
// based on their swagger rules, we do not need to
|
||||
// additionally return error here, let the next ServeHTTPs
|
||||
// handle it appropriately.
|
||||
if len(sessionToken) > 0 {
|
||||
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken)))
|
||||
} else {
|
||||
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "Anonymous"))
|
||||
}
|
||||
ctx := r.Context()
|
||||
claims, _ := auth.ParseClaimsFromToken(string(sessionToken))
|
||||
if claims != nil {
|
||||
// save user session id context
|
||||
ctx = context.WithValue(r.Context(), utils.ContextRequestUserID, claims.STSSessionToken)
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// FileServerMiddleware serves files from the static folder
|
||||
func FileServerMiddleware(next http.Handler) http.Handler {
|
||||
buildFs, err := fs.Sub(portal_ui.GetStaticAssets(), "build")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
spaFileHandler := wrapHandlerSinglePageApplication(requestBounce(http.FileServer(http.FS(buildFs))))
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Server", globalAppName) // do not add version information
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/ws"):
|
||||
serveWS(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api"):
|
||||
next.ServeHTTP(w, r)
|
||||
default:
|
||||
spaFileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type notFoundRedirectRespWr struct {
|
||||
http.ResponseWriter // We embed http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *notFoundRedirectRespWr) WriteHeader(status int) {
|
||||
w.status = status // Store the status for our own use
|
||||
if status != http.StatusNotFound {
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
|
||||
if w.status != http.StatusNotFound {
|
||||
return w.ResponseWriter.Write(p)
|
||||
}
|
||||
return len(p), nil // Lie that we successfully wrote it
|
||||
}
|
||||
|
||||
// handleSPA handles the serving of the React Single Page Application
|
||||
func handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
basePath := "/"
|
||||
// For SPA mode we will replace root base with a sub path if configured unless we received cp=y and cpb=/NEW/BASE
|
||||
if v := r.URL.Query().Get("cp"); v == "y" {
|
||||
if base := r.URL.Query().Get("cpb"); base != "" {
|
||||
// make sure the subpath has a trailing slash
|
||||
if !strings.HasSuffix(base, "/") {
|
||||
base = fmt.Sprintf("%s/", base)
|
||||
}
|
||||
basePath = base
|
||||
}
|
||||
}
|
||||
|
||||
indexPage, err := portal_ui.GetStaticAssets().Open("build/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sts := r.URL.Query().Get("sts")
|
||||
stsAccessKey := r.URL.Query().Get("sts_a")
|
||||
stsSecretKey := r.URL.Query().Get("sts_s")
|
||||
overridenStyles := r.URL.Query().Get("ov_st")
|
||||
|
||||
// if these three parameters are present we are being asked to issue a session with these values
|
||||
if sts != "" && stsAccessKey != "" && stsSecretKey != "" {
|
||||
creds := credentials.NewStaticV4(stsAccessKey, stsSecretKey, sts)
|
||||
consoleCreds := &ConsoleCredentials{
|
||||
ConsoleCredentials: creds,
|
||||
AccountAccessKey: stsAccessKey,
|
||||
}
|
||||
sf := &auth.SessionFeatures{}
|
||||
sf.HideMenu = true
|
||||
sf.ObjectBrowser = true
|
||||
|
||||
if overridenStyles != "" {
|
||||
err := ValidateEncodedStyles(overridenStyles)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sf.CustomStyleOB = overridenStyles
|
||||
}
|
||||
|
||||
sessionID, err := login(consoleCreds, sf)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cookie := NewSessionCookieForConsole(*sessionID)
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
// Allow us to be iframed
|
||||
w.Header().Del("X-Frame-Options")
|
||||
}
|
||||
|
||||
indexPageBytes, err := io.ReadAll(indexPage)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// if we have a seeded basePath. This should override CONSOLE_SUBPATH every time, thus the `if else`
|
||||
if basePath != "/" {
|
||||
indexPageBytes = replaceBaseInIndex(indexPageBytes, basePath)
|
||||
// if we have a custom subpath replace it in
|
||||
} else if getSubPath() != "/" {
|
||||
indexPageBytes = replaceBaseInIndex(indexPageBytes, getSubPath())
|
||||
}
|
||||
indexPageBytes = replaceLicense(indexPageBytes)
|
||||
|
||||
// it's important to force "Content-Type: text/html", because a previous
|
||||
// handler may have already set the content-type to a different value.
|
||||
// (i.e. the FileServer when it detected that it couldn't find the file)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexPageBytes))
|
||||
}
|
||||
|
||||
// wrapHandlerSinglePageApplication handles a http.FileServer returning a 404 and overrides it with index.html
|
||||
func wrapHandlerSinglePageApplication(h http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
handleSPA(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimedb.TypeByExtension(filepath.Ext(r.URL.Path)))
|
||||
nfw := ¬FoundRedirectRespWr{ResponseWriter: w}
|
||||
h.ServeHTTP(nfw, r)
|
||||
if nfw.status == http.StatusNotFound {
|
||||
handleSPA(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nullWriter struct{}
|
||||
|
||||
func (lw nullWriter) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// As soon as server is initialized but not run yet, this function will be called.
|
||||
// If you need to modify a config, store server instance to stop it individually later, this is the place.
|
||||
// This function can be called multiple times, depending on the number of serving schemes.
|
||||
// scheme value will be set accordingly: "http", "https" or "unix"
|
||||
func configureServer(s *http.Server, _, _ string) {
|
||||
// Turn-off random logger by Go net/http
|
||||
s.ErrorLog = log.New(&nullWriter{}, "", 0)
|
||||
}
|
||||
|
||||
func getSubPath() string {
|
||||
subPathOnce.Do(func() {
|
||||
cfgSubPath = parseSubPath(env.Get(SubPath, ""))
|
||||
})
|
||||
return cfgSubPath
|
||||
}
|
||||
|
||||
func parseSubPath(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return SlashSeparator
|
||||
}
|
||||
// Replace all unnecessary `\` to `/`
|
||||
// also add pro-actively at the end.
|
||||
subPath := path.Clean(filepath.ToSlash(v))
|
||||
if !strings.HasPrefix(subPath, SlashSeparator) {
|
||||
subPath = SlashSeparator + subPath
|
||||
}
|
||||
if !strings.HasSuffix(subPath, SlashSeparator) {
|
||||
subPath += SlashSeparator
|
||||
}
|
||||
return subPath
|
||||
}
|
||||
|
||||
func replaceBaseInIndex(indexPageBytes []byte, basePath string) []byte {
|
||||
if basePath != "" {
|
||||
validBasePath := regexp.MustCompile(`^[0-9a-zA-Z\/-]+$`)
|
||||
if !validBasePath.MatchString(basePath) {
|
||||
return indexPageBytes
|
||||
}
|
||||
indexPageStr := string(indexPageBytes)
|
||||
newBase := fmt.Sprintf("<base href=\"%s\"/>", basePath)
|
||||
indexPageStr = strings.Replace(indexPageStr, "<base href=\"/\"/>", newBase, 1)
|
||||
indexPageBytes = []byte(indexPageStr)
|
||||
|
||||
}
|
||||
return indexPageBytes
|
||||
}
|
||||
|
||||
func replaceLicense(indexPageBytes []byte) []byte {
|
||||
indexPageStr := string(indexPageBytes)
|
||||
indexPageBytes = []byte(indexPageStr)
|
||||
return indexPageBytes
|
||||
}
|
||||
|
||||
func requestBounce(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseSubPath(t *testing.T) {
|
||||
type args struct {
|
||||
v string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
args: args{
|
||||
v: "",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
args: args{
|
||||
v: "/",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Double Slash",
|
||||
args: args{
|
||||
v: "//",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "No slashes",
|
||||
args: args{
|
||||
v: "route",
|
||||
},
|
||||
want: "/route/",
|
||||
},
|
||||
{
|
||||
name: "No trailing slashes",
|
||||
args: args{
|
||||
v: "/route",
|
||||
},
|
||||
want: "/route/",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.want, parseSubPath(tt.args.v), "parseSubPath(%v)", tt.args.v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSubPath(t *testing.T) {
|
||||
type args struct {
|
||||
envValue string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
args: args{
|
||||
envValue: "",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
args: args{
|
||||
envValue: "/",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Valid Value",
|
||||
args: args{
|
||||
envValue: "/subpath/",
|
||||
},
|
||||
want: "/subpath/",
|
||||
},
|
||||
{
|
||||
name: "No starting slash",
|
||||
args: args{
|
||||
envValue: "subpath/",
|
||||
},
|
||||
want: "/subpath/",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
t.Setenv(SubPath, tt.args.envValue)
|
||||
defer os.Unsetenv(SubPath)
|
||||
subPathOnce = sync.Once{}
|
||||
assert.Equalf(t, tt.want, getSubPath(), "getSubPath()")
|
||||
})
|
||||
}
|
||||
}
|
||||
279
api/errors.go
279
api/errors.go
@@ -1,279 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDefault = errors.New("an error occurred, please try again")
|
||||
ErrInvalidLogin = errors.New("invalid login")
|
||||
ErrForbidden = errors.New("403 Forbidden")
|
||||
ErrBadRequest = errors.New("400 Bad Request")
|
||||
ErrFileTooLarge = errors.New("413 File too Large")
|
||||
ErrInvalidSession = errors.New("invalid session")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrGroupAlreadyExists = errors.New("error group name already in use")
|
||||
ErrInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value")
|
||||
ErrBucketBodyNotInRequest = errors.New("error bucket body not in request")
|
||||
ErrBucketNameNotInRequest = errors.New("error bucket name not in request")
|
||||
ErrGroupBodyNotInRequest = errors.New("error group body not in request")
|
||||
ErrGroupNameNotInRequest = errors.New("error group name not in request")
|
||||
ErrPolicyNameNotInRequest = errors.New("error policy name not in request")
|
||||
ErrPolicyBodyNotInRequest = errors.New("error policy body not in request")
|
||||
ErrInvalidEncryptionAlgorithm = errors.New("error invalid encryption algorithm")
|
||||
ErrSSENotConfigured = errors.New("error server side encryption configuration not found")
|
||||
ErrBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found")
|
||||
ErrChangePassword = errors.New("error please check your current password")
|
||||
ErrInvalidLicense = errors.New("invalid license key")
|
||||
ErrLicenseNotFound = errors.New("license not found")
|
||||
ErrAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself")
|
||||
ErrAccessDenied = errors.New("access denied")
|
||||
ErrOauth2Provider = errors.New("unable to contact configured identity provider")
|
||||
ErrOauth2Login = errors.New("unable to login using configured identity provider")
|
||||
ErrNonUniqueAccessKey = errors.New("access key already in use")
|
||||
ErrRemoteTierExists = errors.New("specified remote tier already exists")
|
||||
ErrRemoteTierNotFound = errors.New("specified remote tier was not found")
|
||||
ErrRemoteTierUppercase = errors.New("tier name must be in uppercase")
|
||||
ErrRemoteTierBucketNotFound = errors.New("remote tier bucket not found")
|
||||
ErrRemoteInvalidCredentials = errors.New("invalid remote tier credentials")
|
||||
ErrUnableToGetTenantUsage = errors.New("unable to get tenant usage")
|
||||
ErrTooManyNodes = errors.New("cannot request more nodes than what is available in the cluster")
|
||||
ErrTooFewNodes = errors.New("there are not enough nodes in the cluster to support this tenant")
|
||||
ErrTooFewAvailableNodes = errors.New("there is not enough available nodes to satisfy this requirement")
|
||||
ErrFewerThanFourNodes = errors.New("at least 4 nodes are required for a tenant")
|
||||
ErrUnableToGetTenantLogs = errors.New("unable to get tenant logs")
|
||||
ErrUnableToUpdateTenantCertificates = errors.New("unable to update tenant certificates")
|
||||
ErrUpdatingEncryptionConfig = errors.New("unable to update encryption configuration")
|
||||
ErrDeletingEncryptionConfig = errors.New("error disabling tenant encryption")
|
||||
ErrEncryptionConfigNotFound = errors.New("encryption configuration not found")
|
||||
ErrPolicyNotFound = errors.New("policy does not exist")
|
||||
ErrLoginNotAllowed = errors.New("login not allowed")
|
||||
ErrHealthReportFail = errors.New("failure to generate Health report")
|
||||
ErrNetworkError = errors.New("unable to login due to network error")
|
||||
)
|
||||
|
||||
type CodedAPIError struct {
|
||||
Code int
|
||||
APIError *models.APIError
|
||||
}
|
||||
|
||||
// ErrorWithContext :
|
||||
func ErrorWithContext(ctx context.Context, err ...interface{}) *CodedAPIError {
|
||||
errorCode := 500
|
||||
errorMessage := ErrDefault.Error()
|
||||
var detailedMessage string
|
||||
var err1 error
|
||||
var exists bool
|
||||
if len(err) > 0 {
|
||||
if err1, exists = err[0].(error); exists {
|
||||
detailedMessage = err1.Error()
|
||||
var lastError error
|
||||
if len(err) > 1 {
|
||||
if err2, lastExists := err[1].(error); lastExists {
|
||||
lastError = err2
|
||||
}
|
||||
}
|
||||
if err1.Error() == ErrForbidden.Error() {
|
||||
errorCode = 403
|
||||
}
|
||||
if err1.Error() == ErrBadRequest.Error() {
|
||||
errorCode = 400
|
||||
}
|
||||
if err1 == ErrNotFound {
|
||||
errorCode = 404
|
||||
errorMessage = ErrNotFound.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrInvalidLogin) {
|
||||
detailedMessage = ""
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidLogin.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrNetworkError) {
|
||||
detailedMessage = ""
|
||||
errorCode = 503
|
||||
errorMessage = ErrNetworkError.Error()
|
||||
}
|
||||
if strings.Contains(strings.ToLower(err1.Error()), ErrAccessDenied.Error()) {
|
||||
errorCode = 403
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
// If the last error is ErrInvalidLogin, this is a login failure
|
||||
if errors.Is(lastError, ErrInvalidLogin) {
|
||||
detailedMessage = ""
|
||||
errorCode = 401
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if strings.Contains(err1.Error(), ErrLoginNotAllowed.Error()) {
|
||||
detailedMessage = ""
|
||||
errorCode = 400
|
||||
errorMessage = ErrLoginNotAllowed.Error()
|
||||
}
|
||||
// console invalid erasure coding value
|
||||
if errors.Is(err1, ErrInvalidErasureCodingValue) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrInvalidErasureCodingValue.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrBucketBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrBucketBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrBucketNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrBucketNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrGroupBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrGroupBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrGroupNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrGroupNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrPolicyNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrPolicyBodyNotInRequest.Error()
|
||||
}
|
||||
// console invalid session errors
|
||||
if errors.Is(err1, ErrInvalidSession) {
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidSession.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrGroupAlreadyExists) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrGroupAlreadyExists.Error()
|
||||
}
|
||||
// Bucket life cycle not configured
|
||||
if errors.Is(err1, ErrBucketLifeCycleNotConfigured) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrBucketLifeCycleNotConfigured.Error()
|
||||
}
|
||||
// Encryption not configured
|
||||
if errors.Is(err1, ErrSSENotConfigured) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrSSENotConfigured.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrEncryptionConfigNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
// account change password
|
||||
if errors.Is(err1, ErrChangePassword) {
|
||||
errorCode = 403
|
||||
errorMessage = ErrChangePassword.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err1).Code == "SignatureDoesNotMatch" {
|
||||
errorCode = 403
|
||||
errorMessage = ErrChangePassword.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrLicenseNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrLicenseNotFound.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrInvalidLicense) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrInvalidLicense.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrAvoidSelfAccountDelete) {
|
||||
errorCode = 403
|
||||
errorMessage = ErrAvoidSelfAccountDelete.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrAccessDenied) {
|
||||
errorCode = 403
|
||||
errorMessage = ErrAccessDenied.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrPolicyNotFound.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err1).Code == "AccessDenied" {
|
||||
errorCode = 403
|
||||
errorMessage = ErrAccessDenied.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err1).Code == "InvalidAccessKeyId" {
|
||||
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidSession.Error()
|
||||
}
|
||||
// console invalid session errors
|
||||
if madmin.ToErrorResponse(err1).Code == "XMinioAdminNoSuchUser" {
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidSession.Error()
|
||||
}
|
||||
// tiering errors
|
||||
if err1.Error() == ErrRemoteTierExists.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrRemoteTierNotFound.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
|
||||
if err1.Error() == ErrRemoteTierUppercase.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrRemoteTierBucketNotFound.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrRemoteInvalidCredentials.Error() {
|
||||
errorCode = 403
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrFileTooLarge.Error() {
|
||||
errorCode = 413
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
// bucket already exists
|
||||
if minio.ToErrorResponse(err1).Code == "BucketAlreadyOwnedByYou" {
|
||||
errorCode = 400
|
||||
errorMessage = "Bucket already exists"
|
||||
}
|
||||
|
||||
LogError("ErrorWithContext:%v", err...)
|
||||
LogIf(ctx, err1, err...)
|
||||
}
|
||||
|
||||
if len(err) > 1 && err[1] != nil {
|
||||
if err2, ok := err[1].(error); ok {
|
||||
errorMessage = err2.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
return &CodedAPIError{Code: errorCode, APIError: &models.APIError{Message: errorMessage, DetailedMessage: detailedMessage}}
|
||||
}
|
||||
|
||||
// Error receives an errors object and parse it against k8sErrors, returns the right errors code paired with a generic errors message
|
||||
func Error(err ...interface{}) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
return ErrorWithContext(ctx, err...)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
type args struct {
|
||||
err []interface{}
|
||||
}
|
||||
|
||||
type testError struct {
|
||||
name string
|
||||
args args
|
||||
want *CodedAPIError
|
||||
}
|
||||
|
||||
var tests []testError
|
||||
|
||||
type expectedError struct {
|
||||
err error
|
||||
code int
|
||||
}
|
||||
|
||||
appErrors := map[string]expectedError{
|
||||
"ErrDefault": {code: 500, err: ErrDefault},
|
||||
|
||||
"ErrForbidden": {code: 403, err: ErrForbidden},
|
||||
"ErrFileTooLarge": {code: 413, err: ErrFileTooLarge},
|
||||
"ErrInvalidSession": {code: 401, err: ErrInvalidSession},
|
||||
"ErrNotFound": {code: 404, err: ErrNotFound},
|
||||
"ErrGroupAlreadyExists": {code: 400, err: ErrGroupAlreadyExists},
|
||||
"ErrInvalidErasureCodingValue": {code: 400, err: ErrInvalidErasureCodingValue},
|
||||
"ErrBucketBodyNotInRequest": {code: 400, err: ErrBucketBodyNotInRequest},
|
||||
"ErrBucketNameNotInRequest": {code: 400, err: ErrBucketNameNotInRequest},
|
||||
"ErrGroupBodyNotInRequest": {code: 400, err: ErrGroupBodyNotInRequest},
|
||||
"ErrGroupNameNotInRequest": {code: 400, err: ErrGroupNameNotInRequest},
|
||||
"ErrPolicyNameNotInRequest": {code: 400, err: ErrPolicyNameNotInRequest},
|
||||
"ErrPolicyBodyNotInRequest": {code: 400, err: ErrPolicyBodyNotInRequest},
|
||||
"ErrInvalidEncryptionAlgorithm": {code: 500, err: ErrInvalidEncryptionAlgorithm},
|
||||
"ErrSSENotConfigured": {code: 404, err: ErrSSENotConfigured},
|
||||
"ErrChangePassword": {code: 403, err: ErrChangePassword},
|
||||
"ErrInvalidLicense": {code: 404, err: ErrInvalidLicense},
|
||||
"ErrLicenseNotFound": {code: 404, err: ErrLicenseNotFound},
|
||||
"ErrAvoidSelfAccountDelete": {code: 403, err: ErrAvoidSelfAccountDelete},
|
||||
|
||||
"ErrNonUniqueAccessKey": {code: 500, err: ErrNonUniqueAccessKey},
|
||||
"ErrRemoteTierExists": {code: 400, err: ErrRemoteTierExists},
|
||||
"ErrRemoteTierNotFound": {code: 400, err: ErrRemoteTierNotFound},
|
||||
"ErrRemoteTierUppercase": {code: 400, err: ErrRemoteTierUppercase},
|
||||
"ErrRemoteTierBucketNotFound": {code: 400, err: ErrRemoteTierBucketNotFound},
|
||||
"ErrRemoteInvalidCredentials": {code: 403, err: ErrRemoteInvalidCredentials},
|
||||
"ErrTooFewNodes": {code: 500, err: ErrTooFewNodes},
|
||||
"ErrUnableToGetTenantUsage": {code: 500, err: ErrUnableToGetTenantUsage},
|
||||
"ErrTooManyNodes": {code: 500, err: ErrTooManyNodes},
|
||||
"ErrAccessDenied": {code: 403, err: ErrAccessDenied},
|
||||
"ErrTooFewAvailableNodes": {code: 500, err: ErrTooFewAvailableNodes},
|
||||
"ErrFewerThanFourNodes": {code: 500, err: ErrFewerThanFourNodes},
|
||||
"ErrUnableToGetTenantLogs": {code: 500, err: ErrUnableToGetTenantLogs},
|
||||
"ErrUnableToUpdateTenantCertificates": {code: 500, err: ErrUnableToUpdateTenantCertificates},
|
||||
"ErrUpdatingEncryptionConfig": {code: 500, err: ErrUpdatingEncryptionConfig},
|
||||
"ErrDeletingEncryptionConfig": {code: 500, err: ErrDeletingEncryptionConfig},
|
||||
"ErrEncryptionConfigNotFound": {code: 404, err: ErrEncryptionConfigNotFound},
|
||||
}
|
||||
|
||||
for k, e := range appErrors {
|
||||
tests = append(tests, testError{
|
||||
name: fmt.Sprintf("%s error", k),
|
||||
args: args{
|
||||
err: []interface{}{e.err},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: e.code,
|
||||
APIError: &models.APIError{Message: e.err.Error(), DetailedMessage: e.err.Error()},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
tests = append(tests,
|
||||
testError{
|
||||
name: "passing multiple errors but ErrInvalidLogin is last",
|
||||
args: args{
|
||||
err: []interface{}{ErrDefault, ErrInvalidLogin},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: int(401),
|
||||
APIError: &models.APIError{Message: ErrDefault.Error(), DetailedMessage: ""},
|
||||
},
|
||||
})
|
||||
tests = append(tests,
|
||||
testError{
|
||||
name: "login error omits detailedMessage",
|
||||
args: args{
|
||||
err: []interface{}{ErrInvalidLogin},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: int(401),
|
||||
APIError: &models.APIError{Message: ErrInvalidLogin.Error(), DetailedMessage: ""},
|
||||
},
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got := Error(tt.args.err...)
|
||||
assert.Equalf(t, tt.want.Code, got.Code, "Error(%v) Got (%v)", tt.want.Code, got.Code)
|
||||
assert.Equalf(t, tt.want.APIError.DetailedMessage, got.APIError.DetailedMessage, "Error(%s) Got (%s)", tt.want.APIError.DetailedMessage, got.APIError.DetailedMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorWithContext(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
err []interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *CodedAPIError
|
||||
}{
|
||||
{
|
||||
name: "default error",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
err: []interface{}{ErrDefault},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: 500, APIError: &models.APIError{Message: ErrDefault.Error(), DetailedMessage: ErrDefault.Error()},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.want, ErrorWithContext(tt.args.ctx, tt.args.err...), "ErrorWithContext(%v, %v)", tt.args.ctx, tt.args.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
110
api/logs_test.go
110
api/logs_test.go
@@ -1,110 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContext_Load(t *testing.T) {
|
||||
type fields struct {
|
||||
Host string
|
||||
HTTPPort int
|
||||
HTTPSPort int
|
||||
TLSRedirect string
|
||||
TLSCertificate string
|
||||
TLSKey string
|
||||
TLSca string
|
||||
}
|
||||
type args struct {
|
||||
values map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid args",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "on",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid args",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "aaaa",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid port http",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "on",
|
||||
"port": "65536",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid port https",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "on",
|
||||
"port": "65534",
|
||||
"tls-port": "65536",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
c := &Context{}
|
||||
|
||||
fs := flag.NewFlagSet("flags", flag.ContinueOnError)
|
||||
for k, v := range tt.args.values {
|
||||
fs.String(k, v, "ok")
|
||||
}
|
||||
|
||||
ctx := cli.NewContext(nil, fs, &cli.Context{})
|
||||
|
||||
err := c.Load(ctx)
|
||||
if tt.wantErr {
|
||||
assert.NotNilf(t, err, fmt.Sprintf("Load(%v)", err))
|
||||
} else {
|
||||
assert.Nilf(t, err, fmt.Sprintf("Load(%v)", err))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_logInfo(_ *testing.T) {
|
||||
logInfo("message", nil)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package bucket
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// GetMaxShareLinkExpOKCode is the HTTP code returned for type GetMaxShareLinkExpOK
|
||||
const GetMaxShareLinkExpOKCode int = 200
|
||||
|
||||
/*
|
||||
GetMaxShareLinkExpOK A successful response.
|
||||
|
||||
swagger:response getMaxShareLinkExpOK
|
||||
*/
|
||||
type GetMaxShareLinkExpOK struct {
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.MaxShareLinkExpResponse `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewGetMaxShareLinkExpOK creates GetMaxShareLinkExpOK with default headers values
|
||||
func NewGetMaxShareLinkExpOK() *GetMaxShareLinkExpOK {
|
||||
|
||||
return &GetMaxShareLinkExpOK{}
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the get max share link exp o k response
|
||||
func (o *GetMaxShareLinkExpOK) WithPayload(payload *models.MaxShareLinkExpResponse) *GetMaxShareLinkExpOK {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the get max share link exp o k response
|
||||
func (o *GetMaxShareLinkExpOK) SetPayload(payload *models.MaxShareLinkExpResponse) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *GetMaxShareLinkExpOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(200)
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
GetMaxShareLinkExpDefault Generic error response.
|
||||
|
||||
swagger:response getMaxShareLinkExpDefault
|
||||
*/
|
||||
type GetMaxShareLinkExpDefault struct {
|
||||
_statusCode int
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.APIError `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewGetMaxShareLinkExpDefault creates GetMaxShareLinkExpDefault with default headers values
|
||||
func NewGetMaxShareLinkExpDefault(code int) *GetMaxShareLinkExpDefault {
|
||||
if code <= 0 {
|
||||
code = 500
|
||||
}
|
||||
|
||||
return &GetMaxShareLinkExpDefault{
|
||||
_statusCode: code,
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatusCode adds the status to the get max share link exp default response
|
||||
func (o *GetMaxShareLinkExpDefault) WithStatusCode(code int) *GetMaxShareLinkExpDefault {
|
||||
o._statusCode = code
|
||||
return o
|
||||
}
|
||||
|
||||
// SetStatusCode sets the status to the get max share link exp default response
|
||||
func (o *GetMaxShareLinkExpDefault) SetStatusCode(code int) {
|
||||
o._statusCode = code
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the get max share link exp default response
|
||||
func (o *GetMaxShareLinkExpDefault) WithPayload(payload *models.APIError) *GetMaxShareLinkExpDefault {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the get max share link exp default response
|
||||
func (o *GetMaxShareLinkExpDefault) SetPayload(payload *models.APIError) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *GetMaxShareLinkExpDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(o._statusCode)
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,713 +0,0 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package operations
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/runtime/security"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
|
||||
"github.com/minio/console/api/operations/auth"
|
||||
"github.com/minio/console/api/operations/bucket"
|
||||
"github.com/minio/console/api/operations/license"
|
||||
"github.com/minio/console/api/operations/object"
|
||||
"github.com/minio/console/api/operations/public"
|
||||
"github.com/minio/console/api/operations/system"
|
||||
"github.com/minio/console/api/operations/user"
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// NewConsoleAPI creates a new Console instance
|
||||
func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
|
||||
return &ConsoleAPI{
|
||||
handlers: make(map[string]map[string]http.Handler),
|
||||
formats: strfmt.Default,
|
||||
defaultConsumes: "application/json",
|
||||
defaultProduces: "application/json",
|
||||
customConsumers: make(map[string]runtime.Consumer),
|
||||
customProducers: make(map[string]runtime.Producer),
|
||||
PreServerShutdown: func() {},
|
||||
ServerShutdown: func() {},
|
||||
spec: spec,
|
||||
useSwaggerUI: false,
|
||||
ServeError: errors.ServeError,
|
||||
BasicAuthenticator: security.BasicAuth,
|
||||
APIKeyAuthenticator: security.APIKeyAuth,
|
||||
BearerAuthenticator: security.BearerAuth,
|
||||
|
||||
JSONConsumer: runtime.JSONConsumer(),
|
||||
MultipartformConsumer: runtime.DiscardConsumer,
|
||||
|
||||
BinProducer: runtime.ByteStreamProducer(),
|
||||
JSONProducer: runtime.JSONProducer(),
|
||||
|
||||
SystemAdminInfoHandler: system.AdminInfoHandlerFunc(func(params system.AdminInfoParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation system.AdminInfo has not yet been implemented")
|
||||
}),
|
||||
BucketBucketInfoHandler: bucket.BucketInfoHandlerFunc(func(params bucket.BucketInfoParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.BucketInfo has not yet been implemented")
|
||||
}),
|
||||
ObjectDeleteMultipleObjectsHandler: object.DeleteMultipleObjectsHandlerFunc(func(params object.DeleteMultipleObjectsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.DeleteMultipleObjects has not yet been implemented")
|
||||
}),
|
||||
ObjectDeleteObjectHandler: object.DeleteObjectHandlerFunc(func(params object.DeleteObjectParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.DeleteObject has not yet been implemented")
|
||||
}),
|
||||
ObjectDownloadObjectHandler: object.DownloadObjectHandlerFunc(func(params object.DownloadObjectParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.DownloadObject has not yet been implemented")
|
||||
}),
|
||||
ObjectDownloadMultipleObjectsHandler: object.DownloadMultipleObjectsHandlerFunc(func(params object.DownloadMultipleObjectsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.DownloadMultipleObjects has not yet been implemented")
|
||||
}),
|
||||
PublicDownloadSharedObjectHandler: public.DownloadSharedObjectHandlerFunc(func(params public.DownloadSharedObjectParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation public.DownloadSharedObject has not yet been implemented")
|
||||
}),
|
||||
BucketGetBucketQuotaHandler: bucket.GetBucketQuotaHandlerFunc(func(params bucket.GetBucketQuotaParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.GetBucketQuota has not yet been implemented")
|
||||
}),
|
||||
BucketGetBucketRewindHandler: bucket.GetBucketRewindHandlerFunc(func(params bucket.GetBucketRewindParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.GetBucketRewind has not yet been implemented")
|
||||
}),
|
||||
BucketGetBucketVersioningHandler: bucket.GetBucketVersioningHandlerFunc(func(params bucket.GetBucketVersioningParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.GetBucketVersioning has not yet been implemented")
|
||||
}),
|
||||
BucketGetMaxShareLinkExpHandler: bucket.GetMaxShareLinkExpHandlerFunc(func(params bucket.GetMaxShareLinkExpParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.GetMaxShareLinkExp has not yet been implemented")
|
||||
}),
|
||||
ObjectGetObjectMetadataHandler: object.GetObjectMetadataHandlerFunc(func(params object.GetObjectMetadataParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.GetObjectMetadata has not yet been implemented")
|
||||
}),
|
||||
LicenseLicenseAcknowledgeHandler: license.LicenseAcknowledgeHandlerFunc(func(params license.LicenseAcknowledgeParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation license.LicenseAcknowledge has not yet been implemented")
|
||||
}),
|
||||
BucketListBucketsHandler: bucket.ListBucketsHandlerFunc(func(params bucket.ListBucketsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.ListBuckets has not yet been implemented")
|
||||
}),
|
||||
ObjectListObjectsHandler: object.ListObjectsHandlerFunc(func(params object.ListObjectsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.ListObjects has not yet been implemented")
|
||||
}),
|
||||
UserListUsersHandler: user.ListUsersHandlerFunc(func(params user.ListUsersParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user.ListUsers has not yet been implemented")
|
||||
}),
|
||||
AuthLoginHandler: auth.LoginHandlerFunc(func(params auth.LoginParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.Login has not yet been implemented")
|
||||
}),
|
||||
AuthLoginDetailHandler: auth.LoginDetailHandlerFunc(func(params auth.LoginDetailParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.LoginDetail has not yet been implemented")
|
||||
}),
|
||||
AuthLoginOauth2AuthHandler: auth.LoginOauth2AuthHandlerFunc(func(params auth.LoginOauth2AuthParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.LoginOauth2Auth has not yet been implemented")
|
||||
}),
|
||||
AuthLogoutHandler: auth.LogoutHandlerFunc(func(params auth.LogoutParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.Logout has not yet been implemented")
|
||||
}),
|
||||
BucketMakeBucketHandler: bucket.MakeBucketHandlerFunc(func(params bucket.MakeBucketParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.MakeBucket has not yet been implemented")
|
||||
}),
|
||||
ObjectPostBucketsBucketNameObjectsUploadHandler: object.PostBucketsBucketNameObjectsUploadHandlerFunc(func(params object.PostBucketsBucketNameObjectsUploadParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.PostBucketsBucketNameObjectsUpload has not yet been implemented")
|
||||
}),
|
||||
BucketPutBucketTagsHandler: bucket.PutBucketTagsHandlerFunc(func(params bucket.PutBucketTagsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.PutBucketTags has not yet been implemented")
|
||||
}),
|
||||
ObjectPutObjectRestoreHandler: object.PutObjectRestoreHandlerFunc(func(params object.PutObjectRestoreParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.PutObjectRestore has not yet been implemented")
|
||||
}),
|
||||
ObjectPutObjectTagsHandler: object.PutObjectTagsHandlerFunc(func(params object.PutObjectTagsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.PutObjectTags has not yet been implemented")
|
||||
}),
|
||||
AuthSessionCheckHandler: auth.SessionCheckHandlerFunc(func(params auth.SessionCheckParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.SessionCheck has not yet been implemented")
|
||||
}),
|
||||
BucketSetBucketVersioningHandler: bucket.SetBucketVersioningHandlerFunc(func(params bucket.SetBucketVersioningParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation bucket.SetBucketVersioning has not yet been implemented")
|
||||
}),
|
||||
ObjectShareObjectHandler: object.ShareObjectHandlerFunc(func(params object.ShareObjectParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation object.ShareObject has not yet been implemented")
|
||||
}),
|
||||
|
||||
// Applies when the "X-Anonymous" header is set
|
||||
AnonymousAuth: func(token string) (*models.Principal, error) {
|
||||
return nil, errors.NotImplemented("api key auth (anonymous) X-Anonymous from header param [X-Anonymous] has not yet been implemented")
|
||||
},
|
||||
KeyAuth: func(token string, scopes []string) (*models.Principal, error) {
|
||||
return nil, errors.NotImplemented("oauth2 bearer auth (key) has not yet been implemented")
|
||||
},
|
||||
// default authorizer is authorized meaning no requests are blocked
|
||||
APIAuthorizer: security.Authorized(),
|
||||
}
|
||||
}
|
||||
|
||||
/*ConsoleAPI the console API */
|
||||
type ConsoleAPI struct {
|
||||
spec *loads.Document
|
||||
context *middleware.Context
|
||||
handlers map[string]map[string]http.Handler
|
||||
formats strfmt.Registry
|
||||
customConsumers map[string]runtime.Consumer
|
||||
customProducers map[string]runtime.Producer
|
||||
defaultConsumes string
|
||||
defaultProduces string
|
||||
Middleware func(middleware.Builder) http.Handler
|
||||
useSwaggerUI bool
|
||||
|
||||
// BasicAuthenticator generates a runtime.Authenticator from the supplied basic auth function.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator
|
||||
|
||||
// APIKeyAuthenticator generates a runtime.Authenticator from the supplied token auth function.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator
|
||||
|
||||
// BearerAuthenticator generates a runtime.Authenticator from the supplied bearer token auth function.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator
|
||||
|
||||
// JSONConsumer registers a consumer for the following mime types:
|
||||
// - application/json
|
||||
JSONConsumer runtime.Consumer
|
||||
// MultipartformConsumer registers a consumer for the following mime types:
|
||||
// - multipart/form-data
|
||||
MultipartformConsumer runtime.Consumer
|
||||
|
||||
// BinProducer registers a producer for the following mime types:
|
||||
// - application/octet-stream
|
||||
BinProducer runtime.Producer
|
||||
// JSONProducer registers a producer for the following mime types:
|
||||
// - application/json
|
||||
JSONProducer runtime.Producer
|
||||
|
||||
// AnonymousAuth registers a function that takes a token and returns a principal
|
||||
// it performs authentication based on an api key X-Anonymous provided in the header
|
||||
AnonymousAuth func(string) (*models.Principal, error)
|
||||
|
||||
// KeyAuth registers a function that takes an access token and a collection of required scopes and returns a principal
|
||||
// it performs authentication based on an oauth2 bearer token provided in the request
|
||||
KeyAuth func(string, []string) (*models.Principal, error)
|
||||
|
||||
// APIAuthorizer provides access control (ACL/RBAC/ABAC) by providing access to the request and authenticated principal
|
||||
APIAuthorizer runtime.Authorizer
|
||||
|
||||
// SystemAdminInfoHandler sets the operation handler for the admin info operation
|
||||
SystemAdminInfoHandler system.AdminInfoHandler
|
||||
// BucketBucketInfoHandler sets the operation handler for the bucket info operation
|
||||
BucketBucketInfoHandler bucket.BucketInfoHandler
|
||||
// ObjectDeleteMultipleObjectsHandler sets the operation handler for the delete multiple objects operation
|
||||
ObjectDeleteMultipleObjectsHandler object.DeleteMultipleObjectsHandler
|
||||
// ObjectDeleteObjectHandler sets the operation handler for the delete object operation
|
||||
ObjectDeleteObjectHandler object.DeleteObjectHandler
|
||||
// ObjectDownloadObjectHandler sets the operation handler for the download object operation
|
||||
ObjectDownloadObjectHandler object.DownloadObjectHandler
|
||||
// ObjectDownloadMultipleObjectsHandler sets the operation handler for the download multiple objects operation
|
||||
ObjectDownloadMultipleObjectsHandler object.DownloadMultipleObjectsHandler
|
||||
// PublicDownloadSharedObjectHandler sets the operation handler for the download shared object operation
|
||||
PublicDownloadSharedObjectHandler public.DownloadSharedObjectHandler
|
||||
// BucketGetBucketQuotaHandler sets the operation handler for the get bucket quota operation
|
||||
BucketGetBucketQuotaHandler bucket.GetBucketQuotaHandler
|
||||
// BucketGetBucketRewindHandler sets the operation handler for the get bucket rewind operation
|
||||
BucketGetBucketRewindHandler bucket.GetBucketRewindHandler
|
||||
// BucketGetBucketVersioningHandler sets the operation handler for the get bucket versioning operation
|
||||
BucketGetBucketVersioningHandler bucket.GetBucketVersioningHandler
|
||||
// BucketGetMaxShareLinkExpHandler sets the operation handler for the get max share link exp operation
|
||||
BucketGetMaxShareLinkExpHandler bucket.GetMaxShareLinkExpHandler
|
||||
// ObjectGetObjectMetadataHandler sets the operation handler for the get object metadata operation
|
||||
ObjectGetObjectMetadataHandler object.GetObjectMetadataHandler
|
||||
// LicenseLicenseAcknowledgeHandler sets the operation handler for the license acknowledge operation
|
||||
LicenseLicenseAcknowledgeHandler license.LicenseAcknowledgeHandler
|
||||
// BucketListBucketsHandler sets the operation handler for the list buckets operation
|
||||
BucketListBucketsHandler bucket.ListBucketsHandler
|
||||
// ObjectListObjectsHandler sets the operation handler for the list objects operation
|
||||
ObjectListObjectsHandler object.ListObjectsHandler
|
||||
// UserListUsersHandler sets the operation handler for the list users operation
|
||||
UserListUsersHandler user.ListUsersHandler
|
||||
// AuthLoginHandler sets the operation handler for the login operation
|
||||
AuthLoginHandler auth.LoginHandler
|
||||
// AuthLoginDetailHandler sets the operation handler for the login detail operation
|
||||
AuthLoginDetailHandler auth.LoginDetailHandler
|
||||
// AuthLoginOauth2AuthHandler sets the operation handler for the login oauth2 auth operation
|
||||
AuthLoginOauth2AuthHandler auth.LoginOauth2AuthHandler
|
||||
// AuthLogoutHandler sets the operation handler for the logout operation
|
||||
AuthLogoutHandler auth.LogoutHandler
|
||||
// BucketMakeBucketHandler sets the operation handler for the make bucket operation
|
||||
BucketMakeBucketHandler bucket.MakeBucketHandler
|
||||
// ObjectPostBucketsBucketNameObjectsUploadHandler sets the operation handler for the post buckets bucket name objects upload operation
|
||||
ObjectPostBucketsBucketNameObjectsUploadHandler object.PostBucketsBucketNameObjectsUploadHandler
|
||||
// BucketPutBucketTagsHandler sets the operation handler for the put bucket tags operation
|
||||
BucketPutBucketTagsHandler bucket.PutBucketTagsHandler
|
||||
// ObjectPutObjectRestoreHandler sets the operation handler for the put object restore operation
|
||||
ObjectPutObjectRestoreHandler object.PutObjectRestoreHandler
|
||||
// ObjectPutObjectTagsHandler sets the operation handler for the put object tags operation
|
||||
ObjectPutObjectTagsHandler object.PutObjectTagsHandler
|
||||
// AuthSessionCheckHandler sets the operation handler for the session check operation
|
||||
AuthSessionCheckHandler auth.SessionCheckHandler
|
||||
// BucketSetBucketVersioningHandler sets the operation handler for the set bucket versioning operation
|
||||
BucketSetBucketVersioningHandler bucket.SetBucketVersioningHandler
|
||||
// ObjectShareObjectHandler sets the operation handler for the share object operation
|
||||
ObjectShareObjectHandler object.ShareObjectHandler
|
||||
|
||||
// ServeError is called when an error is received, there is a default handler
|
||||
// but you can set your own with this
|
||||
ServeError func(http.ResponseWriter, *http.Request, error)
|
||||
|
||||
// PreServerShutdown is called before the HTTP(S) server is shutdown
|
||||
// This allows for custom functions to get executed before the HTTP(S) server stops accepting traffic
|
||||
PreServerShutdown func()
|
||||
|
||||
// ServerShutdown is called when the HTTP(S) server is shut down and done
|
||||
// handling all active connections and does not accept connections any more
|
||||
ServerShutdown func()
|
||||
|
||||
// Custom command line argument groups with their descriptions
|
||||
CommandLineOptionsGroups []swag.CommandLineOptionsGroup
|
||||
|
||||
// User defined logger function.
|
||||
Logger func(string, ...interface{})
|
||||
}
|
||||
|
||||
// UseRedoc for documentation at /docs
|
||||
func (o *ConsoleAPI) UseRedoc() {
|
||||
o.useSwaggerUI = false
|
||||
}
|
||||
|
||||
// UseSwaggerUI for documentation at /docs
|
||||
func (o *ConsoleAPI) UseSwaggerUI() {
|
||||
o.useSwaggerUI = true
|
||||
}
|
||||
|
||||
// SetDefaultProduces sets the default produces media type
|
||||
func (o *ConsoleAPI) SetDefaultProduces(mediaType string) {
|
||||
o.defaultProduces = mediaType
|
||||
}
|
||||
|
||||
// SetDefaultConsumes returns the default consumes media type
|
||||
func (o *ConsoleAPI) SetDefaultConsumes(mediaType string) {
|
||||
o.defaultConsumes = mediaType
|
||||
}
|
||||
|
||||
// SetSpec sets a spec that will be served for the clients.
|
||||
func (o *ConsoleAPI) SetSpec(spec *loads.Document) {
|
||||
o.spec = spec
|
||||
}
|
||||
|
||||
// DefaultProduces returns the default produces media type
|
||||
func (o *ConsoleAPI) DefaultProduces() string {
|
||||
return o.defaultProduces
|
||||
}
|
||||
|
||||
// DefaultConsumes returns the default consumes media type
|
||||
func (o *ConsoleAPI) DefaultConsumes() string {
|
||||
return o.defaultConsumes
|
||||
}
|
||||
|
||||
// Formats returns the registered string formats
|
||||
func (o *ConsoleAPI) Formats() strfmt.Registry {
|
||||
return o.formats
|
||||
}
|
||||
|
||||
// RegisterFormat registers a custom format validator
|
||||
func (o *ConsoleAPI) RegisterFormat(name string, format strfmt.Format, validator strfmt.Validator) {
|
||||
o.formats.Add(name, format, validator)
|
||||
}
|
||||
|
||||
// Validate validates the registrations in the ConsoleAPI
|
||||
func (o *ConsoleAPI) Validate() error {
|
||||
var unregistered []string
|
||||
|
||||
if o.JSONConsumer == nil {
|
||||
unregistered = append(unregistered, "JSONConsumer")
|
||||
}
|
||||
if o.MultipartformConsumer == nil {
|
||||
unregistered = append(unregistered, "MultipartformConsumer")
|
||||
}
|
||||
|
||||
if o.BinProducer == nil {
|
||||
unregistered = append(unregistered, "BinProducer")
|
||||
}
|
||||
if o.JSONProducer == nil {
|
||||
unregistered = append(unregistered, "JSONProducer")
|
||||
}
|
||||
|
||||
if o.AnonymousAuth == nil {
|
||||
unregistered = append(unregistered, "XAnonymousAuth")
|
||||
}
|
||||
if o.KeyAuth == nil {
|
||||
unregistered = append(unregistered, "KeyAuth")
|
||||
}
|
||||
|
||||
if o.SystemAdminInfoHandler == nil {
|
||||
unregistered = append(unregistered, "system.AdminInfoHandler")
|
||||
}
|
||||
if o.BucketBucketInfoHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.BucketInfoHandler")
|
||||
}
|
||||
if o.ObjectDeleteMultipleObjectsHandler == nil {
|
||||
unregistered = append(unregistered, "object.DeleteMultipleObjectsHandler")
|
||||
}
|
||||
if o.ObjectDeleteObjectHandler == nil {
|
||||
unregistered = append(unregistered, "object.DeleteObjectHandler")
|
||||
}
|
||||
if o.ObjectDownloadObjectHandler == nil {
|
||||
unregistered = append(unregistered, "object.DownloadObjectHandler")
|
||||
}
|
||||
if o.ObjectDownloadMultipleObjectsHandler == nil {
|
||||
unregistered = append(unregistered, "object.DownloadMultipleObjectsHandler")
|
||||
}
|
||||
if o.PublicDownloadSharedObjectHandler == nil {
|
||||
unregistered = append(unregistered, "public.DownloadSharedObjectHandler")
|
||||
}
|
||||
if o.BucketGetBucketQuotaHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.GetBucketQuotaHandler")
|
||||
}
|
||||
if o.BucketGetBucketRewindHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.GetBucketRewindHandler")
|
||||
}
|
||||
if o.BucketGetBucketVersioningHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.GetBucketVersioningHandler")
|
||||
}
|
||||
if o.BucketGetMaxShareLinkExpHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.GetMaxShareLinkExpHandler")
|
||||
}
|
||||
if o.ObjectGetObjectMetadataHandler == nil {
|
||||
unregistered = append(unregistered, "object.GetObjectMetadataHandler")
|
||||
}
|
||||
if o.LicenseLicenseAcknowledgeHandler == nil {
|
||||
unregistered = append(unregistered, "license.LicenseAcknowledgeHandler")
|
||||
}
|
||||
if o.BucketListBucketsHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.ListBucketsHandler")
|
||||
}
|
||||
if o.ObjectListObjectsHandler == nil {
|
||||
unregistered = append(unregistered, "object.ListObjectsHandler")
|
||||
}
|
||||
if o.UserListUsersHandler == nil {
|
||||
unregistered = append(unregistered, "user.ListUsersHandler")
|
||||
}
|
||||
if o.AuthLoginHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LoginHandler")
|
||||
}
|
||||
if o.AuthLoginDetailHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LoginDetailHandler")
|
||||
}
|
||||
if o.AuthLoginOauth2AuthHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LoginOauth2AuthHandler")
|
||||
}
|
||||
if o.AuthLogoutHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LogoutHandler")
|
||||
}
|
||||
if o.BucketMakeBucketHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.MakeBucketHandler")
|
||||
}
|
||||
if o.ObjectPostBucketsBucketNameObjectsUploadHandler == nil {
|
||||
unregistered = append(unregistered, "object.PostBucketsBucketNameObjectsUploadHandler")
|
||||
}
|
||||
if o.BucketPutBucketTagsHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.PutBucketTagsHandler")
|
||||
}
|
||||
if o.ObjectPutObjectRestoreHandler == nil {
|
||||
unregistered = append(unregistered, "object.PutObjectRestoreHandler")
|
||||
}
|
||||
if o.ObjectPutObjectTagsHandler == nil {
|
||||
unregistered = append(unregistered, "object.PutObjectTagsHandler")
|
||||
}
|
||||
if o.AuthSessionCheckHandler == nil {
|
||||
unregistered = append(unregistered, "auth.SessionCheckHandler")
|
||||
}
|
||||
if o.BucketSetBucketVersioningHandler == nil {
|
||||
unregistered = append(unregistered, "bucket.SetBucketVersioningHandler")
|
||||
}
|
||||
if o.ObjectShareObjectHandler == nil {
|
||||
unregistered = append(unregistered, "object.ShareObjectHandler")
|
||||
}
|
||||
|
||||
if len(unregistered) > 0 {
|
||||
return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeErrorFor gets a error handler for a given operation id
|
||||
func (o *ConsoleAPI) ServeErrorFor(operationID string) func(http.ResponseWriter, *http.Request, error) {
|
||||
return o.ServeError
|
||||
}
|
||||
|
||||
// AuthenticatorsFor gets the authenticators for the specified security schemes
|
||||
func (o *ConsoleAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator {
|
||||
result := make(map[string]runtime.Authenticator)
|
||||
for name := range schemes {
|
||||
switch name {
|
||||
case "anonymous":
|
||||
scheme := schemes[name]
|
||||
result[name] = o.APIKeyAuthenticator(scheme.Name, scheme.In, func(token string) (interface{}, error) {
|
||||
return o.AnonymousAuth(token)
|
||||
})
|
||||
|
||||
case "key":
|
||||
result[name] = o.BearerAuthenticator(name, func(token string, scopes []string) (interface{}, error) {
|
||||
return o.KeyAuth(token, scopes)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Authorizer returns the registered authorizer
|
||||
func (o *ConsoleAPI) Authorizer() runtime.Authorizer {
|
||||
return o.APIAuthorizer
|
||||
}
|
||||
|
||||
// ConsumersFor gets the consumers for the specified media types.
|
||||
// MIME type parameters are ignored here.
|
||||
func (o *ConsoleAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer {
|
||||
result := make(map[string]runtime.Consumer, len(mediaTypes))
|
||||
for _, mt := range mediaTypes {
|
||||
switch mt {
|
||||
case "application/json":
|
||||
result["application/json"] = o.JSONConsumer
|
||||
case "multipart/form-data":
|
||||
result["multipart/form-data"] = o.MultipartformConsumer
|
||||
}
|
||||
|
||||
if c, ok := o.customConsumers[mt]; ok {
|
||||
result[mt] = c
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ProducersFor gets the producers for the specified media types.
|
||||
// MIME type parameters are ignored here.
|
||||
func (o *ConsoleAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer {
|
||||
result := make(map[string]runtime.Producer, len(mediaTypes))
|
||||
for _, mt := range mediaTypes {
|
||||
switch mt {
|
||||
case "application/octet-stream":
|
||||
result["application/octet-stream"] = o.BinProducer
|
||||
case "application/json":
|
||||
result["application/json"] = o.JSONProducer
|
||||
}
|
||||
|
||||
if p, ok := o.customProducers[mt]; ok {
|
||||
result[mt] = p
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// HandlerFor gets a http.Handler for the provided operation method and path
|
||||
func (o *ConsoleAPI) HandlerFor(method, path string) (http.Handler, bool) {
|
||||
if o.handlers == nil {
|
||||
return nil, false
|
||||
}
|
||||
um := strings.ToUpper(method)
|
||||
if _, ok := o.handlers[um]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
if path == "/" {
|
||||
path = ""
|
||||
}
|
||||
h, ok := o.handlers[um][path]
|
||||
return h, ok
|
||||
}
|
||||
|
||||
// Context returns the middleware context for the console API
|
||||
func (o *ConsoleAPI) Context() *middleware.Context {
|
||||
if o.context == nil {
|
||||
o.context = middleware.NewRoutableContext(o.spec, o, nil)
|
||||
}
|
||||
|
||||
return o.context
|
||||
}
|
||||
|
||||
func (o *ConsoleAPI) initHandlerCache() {
|
||||
o.Context() // don't care about the result, just that the initialization happened
|
||||
if o.handlers == nil {
|
||||
o.handlers = make(map[string]map[string]http.Handler)
|
||||
}
|
||||
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/admin/info"] = system.NewAdminInfo(o.context, o.SystemAdminInfoHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{name}"] = bucket.NewBucketInfo(o.context, o.BucketBucketInfoHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/buckets/{bucket_name}/delete-objects"] = object.NewDeleteMultipleObjects(o.context, o.ObjectDeleteMultipleObjectsHandler)
|
||||
if o.handlers["DELETE"] == nil {
|
||||
o.handlers["DELETE"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["DELETE"]["/buckets/{bucket_name}/objects"] = object.NewDeleteObject(o.context, o.ObjectDeleteObjectHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{bucket_name}/objects/download"] = object.NewDownloadObject(o.context, o.ObjectDownloadObjectHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/buckets/{bucket_name}/objects/download-multiple"] = object.NewDownloadMultipleObjects(o.context, o.ObjectDownloadMultipleObjectsHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/download-shared-object/{url}"] = public.NewDownloadSharedObject(o.context, o.PublicDownloadSharedObjectHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{name}/quota"] = bucket.NewGetBucketQuota(o.context, o.BucketGetBucketQuotaHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{bucket_name}/rewind/{date}"] = bucket.NewGetBucketRewind(o.context, o.BucketGetBucketRewindHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{bucket_name}/versioning"] = bucket.NewGetBucketVersioning(o.context, o.BucketGetBucketVersioningHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/max-share-exp"] = bucket.NewGetMaxShareLinkExp(o.context, o.BucketGetMaxShareLinkExpHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{bucket_name}/objects/metadata"] = object.NewGetObjectMetadata(o.context, o.ObjectGetObjectMetadataHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/license/acknowledge"] = license.NewLicenseAcknowledge(o.context, o.LicenseLicenseAcknowledgeHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets"] = bucket.NewListBuckets(o.context, o.BucketListBucketsHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{bucket_name}/objects"] = object.NewListObjects(o.context, o.ObjectListObjectsHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/users"] = user.NewListUsers(o.context, o.UserListUsersHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/login"] = auth.NewLogin(o.context, o.AuthLoginHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/login"] = auth.NewLoginDetail(o.context, o.AuthLoginDetailHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/login/oauth2/auth"] = auth.NewLoginOauth2Auth(o.context, o.AuthLoginOauth2AuthHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/logout"] = auth.NewLogout(o.context, o.AuthLogoutHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/buckets"] = bucket.NewMakeBucket(o.context, o.BucketMakeBucketHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/buckets/{bucket_name}/objects/upload"] = object.NewPostBucketsBucketNameObjectsUpload(o.context, o.ObjectPostBucketsBucketNameObjectsUploadHandler)
|
||||
if o.handlers["PUT"] == nil {
|
||||
o.handlers["PUT"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["PUT"]["/buckets/{bucket_name}/tags"] = bucket.NewPutBucketTags(o.context, o.BucketPutBucketTagsHandler)
|
||||
if o.handlers["PUT"] == nil {
|
||||
o.handlers["PUT"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["PUT"]["/buckets/{bucket_name}/objects/restore"] = object.NewPutObjectRestore(o.context, o.ObjectPutObjectRestoreHandler)
|
||||
if o.handlers["PUT"] == nil {
|
||||
o.handlers["PUT"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["PUT"]["/buckets/{bucket_name}/objects/tags"] = object.NewPutObjectTags(o.context, o.ObjectPutObjectTagsHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/session"] = auth.NewSessionCheck(o.context, o.AuthSessionCheckHandler)
|
||||
if o.handlers["PUT"] == nil {
|
||||
o.handlers["PUT"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["PUT"]["/buckets/{bucket_name}/versioning"] = bucket.NewSetBucketVersioning(o.context, o.BucketSetBucketVersioningHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/buckets/{bucket_name}/objects/share"] = object.NewShareObject(o.context, o.ObjectShareObjectHandler)
|
||||
}
|
||||
|
||||
// Serve creates a http handler to serve the API over HTTP
|
||||
// can be used directly in http.ListenAndServe(":8000", api.Serve(nil))
|
||||
func (o *ConsoleAPI) Serve(builder middleware.Builder) http.Handler {
|
||||
o.Init()
|
||||
|
||||
if o.Middleware != nil {
|
||||
return o.Middleware(builder)
|
||||
}
|
||||
if o.useSwaggerUI {
|
||||
return o.context.APIHandlerSwaggerUI(builder)
|
||||
}
|
||||
return o.context.APIHandler(builder)
|
||||
}
|
||||
|
||||
// Init allows you to just initialize the handler cache, you can then recompose the middleware as you see fit
|
||||
func (o *ConsoleAPI) Init() {
|
||||
if len(o.handlers) == 0 {
|
||||
o.initHandlerCache()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterConsumer allows you to add (or override) a consumer for a media type.
|
||||
func (o *ConsoleAPI) RegisterConsumer(mediaType string, consumer runtime.Consumer) {
|
||||
o.customConsumers[mediaType] = consumer
|
||||
}
|
||||
|
||||
// RegisterProducer allows you to add (or override) a producer for a media type.
|
||||
func (o *ConsoleAPI) RegisterProducer(mediaType string, producer runtime.Producer) {
|
||||
o.customProducers[mediaType] = producer
|
||||
}
|
||||
|
||||
// AddMiddlewareFor adds a http middleware to existing handler
|
||||
func (o *ConsoleAPI) AddMiddlewareFor(method, path string, builder middleware.Builder) {
|
||||
um := strings.ToUpper(method)
|
||||
if path == "/" {
|
||||
path = ""
|
||||
}
|
||||
o.Init()
|
||||
if h, ok := o.handlers[um][path]; ok {
|
||||
o.handlers[um][path] = builder(h)
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package object
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// DownloadMultipleObjectsOKCode is the HTTP code returned for type DownloadMultipleObjectsOK
|
||||
const DownloadMultipleObjectsOKCode int = 200
|
||||
|
||||
/*
|
||||
DownloadMultipleObjectsOK A successful response.
|
||||
|
||||
swagger:response downloadMultipleObjectsOK
|
||||
*/
|
||||
type DownloadMultipleObjectsOK struct {
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload io.ReadCloser `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewDownloadMultipleObjectsOK creates DownloadMultipleObjectsOK with default headers values
|
||||
func NewDownloadMultipleObjectsOK() *DownloadMultipleObjectsOK {
|
||||
|
||||
return &DownloadMultipleObjectsOK{}
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the download multiple objects o k response
|
||||
func (o *DownloadMultipleObjectsOK) WithPayload(payload io.ReadCloser) *DownloadMultipleObjectsOK {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the download multiple objects o k response
|
||||
func (o *DownloadMultipleObjectsOK) SetPayload(payload io.ReadCloser) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *DownloadMultipleObjectsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(200)
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DownloadMultipleObjectsDefault Generic error response.
|
||||
|
||||
swagger:response downloadMultipleObjectsDefault
|
||||
*/
|
||||
type DownloadMultipleObjectsDefault struct {
|
||||
_statusCode int
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.APIError `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewDownloadMultipleObjectsDefault creates DownloadMultipleObjectsDefault with default headers values
|
||||
func NewDownloadMultipleObjectsDefault(code int) *DownloadMultipleObjectsDefault {
|
||||
if code <= 0 {
|
||||
code = 500
|
||||
}
|
||||
|
||||
return &DownloadMultipleObjectsDefault{
|
||||
_statusCode: code,
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatusCode adds the status to the download multiple objects default response
|
||||
func (o *DownloadMultipleObjectsDefault) WithStatusCode(code int) *DownloadMultipleObjectsDefault {
|
||||
o._statusCode = code
|
||||
return o
|
||||
}
|
||||
|
||||
// SetStatusCode sets the status to the download multiple objects default response
|
||||
func (o *DownloadMultipleObjectsDefault) SetStatusCode(code int) {
|
||||
o._statusCode = code
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the download multiple objects default response
|
||||
func (o *DownloadMultipleObjectsDefault) WithPayload(payload *models.APIError) *DownloadMultipleObjectsDefault {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the download multiple objects default response
|
||||
func (o *DownloadMultipleObjectsDefault) SetPayload(payload *models.APIError) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *DownloadMultipleObjectsDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(o._statusCode)
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package public
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// DownloadSharedObjectOKCode is the HTTP code returned for type DownloadSharedObjectOK
|
||||
const DownloadSharedObjectOKCode int = 200
|
||||
|
||||
/*
|
||||
DownloadSharedObjectOK A successful response.
|
||||
|
||||
swagger:response downloadSharedObjectOK
|
||||
*/
|
||||
type DownloadSharedObjectOK struct {
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload io.ReadCloser `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewDownloadSharedObjectOK creates DownloadSharedObjectOK with default headers values
|
||||
func NewDownloadSharedObjectOK() *DownloadSharedObjectOK {
|
||||
|
||||
return &DownloadSharedObjectOK{}
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the download shared object o k response
|
||||
func (o *DownloadSharedObjectOK) WithPayload(payload io.ReadCloser) *DownloadSharedObjectOK {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the download shared object o k response
|
||||
func (o *DownloadSharedObjectOK) SetPayload(payload io.ReadCloser) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *DownloadSharedObjectOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(200)
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DownloadSharedObjectDefault Generic error response.
|
||||
|
||||
swagger:response downloadSharedObjectDefault
|
||||
*/
|
||||
type DownloadSharedObjectDefault struct {
|
||||
_statusCode int
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.APIError `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewDownloadSharedObjectDefault creates DownloadSharedObjectDefault with default headers values
|
||||
func NewDownloadSharedObjectDefault(code int) *DownloadSharedObjectDefault {
|
||||
if code <= 0 {
|
||||
code = 500
|
||||
}
|
||||
|
||||
return &DownloadSharedObjectDefault{
|
||||
_statusCode: code,
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatusCode adds the status to the download shared object default response
|
||||
func (o *DownloadSharedObjectDefault) WithStatusCode(code int) *DownloadSharedObjectDefault {
|
||||
o._statusCode = code
|
||||
return o
|
||||
}
|
||||
|
||||
// SetStatusCode sets the status to the download shared object default response
|
||||
func (o *DownloadSharedObjectDefault) SetStatusCode(code int) {
|
||||
o._statusCode = code
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the download shared object default response
|
||||
func (o *DownloadSharedObjectDefault) WithPayload(payload *models.APIError) *DownloadSharedObjectDefault {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the download shared object default response
|
||||
func (o *DownloadSharedObjectDefault) SetPayload(payload *models.APIError) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *DownloadSharedObjectDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(o._statusCode)
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
// ReplacePolicyVariables replaces known variables from policies with known values
|
||||
func ReplacePolicyVariables(claims map[string]interface{}, accountInfo *madmin.AccountInfo) json.RawMessage {
|
||||
// AWS Variables
|
||||
rawPolicy := bytes.ReplaceAll(accountInfo.Policy, []byte("${aws:username}"), []byte(accountInfo.AccountName))
|
||||
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte("${aws:userid}"), []byte(accountInfo.AccountName))
|
||||
// JWT Variables
|
||||
rawPolicy = replaceJwtVariables(rawPolicy, claims)
|
||||
// LDAP Variables
|
||||
rawPolicy = replaceLDAPVariables(rawPolicy, claims)
|
||||
return rawPolicy
|
||||
}
|
||||
|
||||
func replaceJwtVariables(rawPolicy []byte, claims map[string]interface{}) json.RawMessage {
|
||||
// list of valid JWT fields we will replace in policy if they are in the response
|
||||
jwtFields := []string{
|
||||
"sub",
|
||||
"iss",
|
||||
"aud",
|
||||
"jti",
|
||||
"upn",
|
||||
"name",
|
||||
"groups",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"middle_name",
|
||||
"nickname",
|
||||
"preferred_username",
|
||||
"profile",
|
||||
"picture",
|
||||
"website",
|
||||
"email",
|
||||
"gender",
|
||||
"birthdate",
|
||||
"phone_number",
|
||||
"address",
|
||||
"scope",
|
||||
"client_id",
|
||||
}
|
||||
// check which fields are in the claims and replace as variable by casting the value to string
|
||||
for _, field := range jwtFields {
|
||||
if val, ok := claims[field]; ok {
|
||||
variable := fmt.Sprintf("${jwt:%s}", field)
|
||||
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte(variable), []byte(fmt.Sprintf("%v", val)))
|
||||
}
|
||||
}
|
||||
return rawPolicy
|
||||
}
|
||||
|
||||
// ReplacePolicyVariables replaces known variables from policies with known values
|
||||
func replaceLDAPVariables(rawPolicy []byte, claims map[string]interface{}) json.RawMessage {
|
||||
// replace ${ldap:user}
|
||||
if val, ok := claims["ldapUser"]; ok {
|
||||
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte("${ldap:user}"), []byte(fmt.Sprintf("%v", val)))
|
||||
}
|
||||
// replace ${ldap:username}
|
||||
if val, ok := claims["ldapUsername"]; ok {
|
||||
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte("${ldap:username}"), []byte(fmt.Sprintf("%v", val)))
|
||||
}
|
||||
return rawPolicy
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
minioIAMPolicy "github.com/minio/pkg/v3/policy"
|
||||
)
|
||||
|
||||
func TestReplacePolicyVariables(t *testing.T) {
|
||||
type args struct {
|
||||
claims map[string]interface{}
|
||||
accountInfo *madmin.AccountInfo
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Bad Policy",
|
||||
args: args{
|
||||
claims: nil,
|
||||
accountInfo: &madmin.AccountInfo{
|
||||
AccountName: "test",
|
||||
Server: madmin.BackendInfo{},
|
||||
Policy: []byte(""),
|
||||
Buckets: nil,
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Replace basic AWS",
|
||||
args: args{
|
||||
claims: nil,
|
||||
accountInfo: &madmin.AccountInfo{
|
||||
AccountName: "test",
|
||||
Server: madmin.BackendInfo{},
|
||||
Policy: []byte(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::${aws:username}",
|
||||
"arn:aws:s3:::${aws:userid}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
Buckets: nil,
|
||||
},
|
||||
},
|
||||
want: `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::test",
|
||||
"arn:aws:s3:::test"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got := ReplacePolicyVariables(tt.args.claims, tt.args.accountInfo)
|
||||
policy, err := minioIAMPolicy.ParseConfig(bytes.NewReader(got))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ReplacePolicyVariables() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
wantPolicy, err := minioIAMPolicy.ParseConfig(bytes.NewReader([]byte(tt.want)))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ReplacePolicyVariables() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(policy, wantPolicy) {
|
||||
t.Errorf("ReplacePolicyVariables() = %s, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2024 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/api/operations/public"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
)
|
||||
|
||||
func registerPublicObjectsHandlers(api *operations.ConsoleAPI) {
|
||||
api.PublicDownloadSharedObjectHandler = public.DownloadSharedObjectHandlerFunc(func(params public.DownloadSharedObjectParams) middleware.Responder {
|
||||
resp, err := getDownloadPublicObjectResponse(params)
|
||||
if err != nil {
|
||||
return public.NewDownloadSharedObjectDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
func getDownloadPublicObjectResponse(params public.DownloadSharedObjectParams) (middleware.Responder, *CodedAPIError) {
|
||||
ctx := params.HTTPRequest.Context()
|
||||
|
||||
inputURLDecoded, err := decodeMinIOStringURL(params.URL)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
if inputURLDecoded == nil {
|
||||
return nil, ErrorWithContext(ctx, ErrDefault, fmt.Errorf("decoded url is null"))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, *inputURLDecoded, nil)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
clnt := PrepareConsoleHTTPClient(getClientIP(params.HTTPRequest))
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
http.Error(rw, resp.Status, resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
urlObj, err := url.Parse(*inputURLDecoded)
|
||||
if err != nil {
|
||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Add the filename
|
||||
_, objectName := url2BucketAndObject(urlObj)
|
||||
escapedName := url.PathEscape(objectName)
|
||||
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", escapedName))
|
||||
|
||||
_, err = io.Copy(rw, resp.Body)
|
||||
if err != nil {
|
||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
// decodeMinIOStringURL decodes url and validates is a MinIO url endpoint
|
||||
func decodeMinIOStringURL(inputURL string) (*string, error) {
|
||||
decodedURL, err := base64.RawURLEncoding.DecodeString(inputURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate input URL
|
||||
parsedURL, err := xnet.ParseHTTPURL(string(decodedURL))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ensure incoming url points to MinIO Server
|
||||
minIOHost := getMinIOEndpoint()
|
||||
if parsedURL.Host != minIOHost {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
return swag.String(string(decodedURL)), nil
|
||||
}
|
||||
|
||||
func url2BucketAndObject(u *url.URL) (bucketName, objectName string) {
|
||||
tokens := splitStr(u.Path, "/", 3)
|
||||
return tokens[1], tokens[2]
|
||||
}
|
||||
|
||||
// splitStr splits a string into n parts, empty strings are added
|
||||
// if we are not able to reach n elements
|
||||
func splitStr(path, sep string, n int) []string {
|
||||
splits := strings.SplitN(path, sep, n)
|
||||
// Add empty strings if we found elements less than nr
|
||||
for i := n - len(splits); i > 0; i-- {
|
||||
splits = append(splits, "")
|
||||
}
|
||||
return splits
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2024 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decodeMinIOStringURL(t *testing.T) {
|
||||
tAssert := assert.New(t)
|
||||
type args struct {
|
||||
encodedURL string
|
||||
}
|
||||
tests := []struct {
|
||||
test string
|
||||
args args
|
||||
wantError *string
|
||||
expected *string
|
||||
}{
|
||||
{
|
||||
test: "valid encoded minIO URL returns decoded URL string", // http://localhost:9000/...
|
||||
args: args{
|
||||
encodedURL: "aHR0cDovL2xvY2FsaG9zdDo5MDAwL2J1Y2tldDEyMy9BdWRpbyUyMGljb24lMjgxJTI5LnN2Zz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPVVCTzFMMUM3VTg3UDFCUDI1MVRTJTJGMjAyNDA0MDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNDA1VDIxMDEzM1omWC1BbXotRXhwaXJlcz00MzIwMCZYLUFtei1TZWN1cml0eS1Ub2tlbj1leUpoYkdjaU9pSklVelV4TWlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKaFkyTmxjM05MWlhraU9pSlZRazh4VERGRE4xVTROMUF4UWxBeU5URlVVeUlzSW1WNGNDSTZNVGN4TWpNNU5EQTRPU3dpY0dGeVpXNTBJam9pYldsdWFXOWhaRzFwYmlKOS5WLUtEZ3JMTVVCbG5KSEtYNlZ4SGw5LUFfLVBGRVdvazJkcFRxLTQ2YmxMbUxzdWVUeHNoVmFZNERad0dmb200VFQ1azhwaFVmZ2pjUWFuc25icmtlQSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmdmVyc2lvbklkPW51bGwmWC1BbXotU2lnbmF0dXJlPTA3Y2FkM2ViMmE2NzIyYjViYWVkMDljNmYxZmU0YTcwMWJmMTJmNDhlMTYyOGI5ZDQ1YzAxMWQ1OTU1Njc4NDU",
|
||||
},
|
||||
wantError: nil,
|
||||
expected: swag.String("http://localhost:9000/bucket123/Audio%20icon%281%29.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=UBO1L1C7U87P1BP251TS%2F20240405%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240405T210133Z&X-Amz-Expires=43200&X-Amz-Security-Token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJVQk8xTDFDN1U4N1AxQlAyNTFUUyIsImV4cCI6MTcxMjM5NDA4OSwicGFyZW50IjoibWluaW9hZG1pbiJ9.V-KDgrLMUBlnJHKX6VxHl9-A_-PFEWok2dpTq-46blLmLsueTxshVaY4DZwGfom4TT5k8phUfgjcQansnbrkeA&X-Amz-SignedHeaders=host&versionId=null&X-Amz-Signature=07cad3eb2a6722b5baed09c6f1fe4a701bf12f48e1628b9d45c011d595567845"),
|
||||
},
|
||||
{
|
||||
test: "valid encoded url but not coming from MinIO server returns forbidden error", // http://non-minio-host:9000/...
|
||||
args: args{
|
||||
encodedURL: "aHR0cDovL25vbi1taW5pby1ob3N0OjkwMDAvYnVja2V0MTIzL0F1ZGlvJTIwaWNvbiUyODElMjkuc3ZnP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9VUJPMUwxQzdVODdQMUJQMjUxVFMlMkYyMDI0MDQwNSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA0MDVUMjEwMTMzWiZYLUFtei1FeHBpcmVzPTQzMjAwJlgtQW16LVNlY3VyaXR5LVRva2VuPWV5SmhiR2NpT2lKSVV6VXhNaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpoWTJObGMzTkxaWGtpT2lKVlFrOHhUREZETjFVNE4xQXhRbEF5TlRGVVV5SXNJbVY0Y0NJNk1UY3hNak01TkRBNE9Td2ljR0Z5Wlc1MElqb2liV2x1YVc5aFpHMXBiaUo5LlYtS0RnckxNVUJsbkpIS1g2VnhIbDktQV8tUEZFV29rMmRwVHEtNDZibExtTHN1ZVR4c2hWYVk0RFp3R2ZvbTRUVDVrOHBoVWZnamNRYW5zbmJya2VBJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZ2ZXJzaW9uSWQ9bnVsbCZYLUFtei1TaWduYXR1cmU9MDdjYWQzZWIyYTY3MjJiNWJhZWQwOWM2ZjFmZTRhNzAxYmYxMmY0OGUxNjI4YjlkNDVjMDExZDU5NTU2Nzg0NQ",
|
||||
},
|
||||
wantError: swag.String("403 Forbidden"),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
test: "valid encoded url but not coming from MinIO server port returns forbidden error", // other port http://localhost:8902/...
|
||||
args: args{
|
||||
encodedURL: "aHR0cDovL2xvY2FsaG9zdDo4OTAyL2J1Y2tldDEyMy9BdWRpbyUyMGljb24lMjgxJTI5LnN2Zz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPVVCTzFMMUM3VTg3UDFCUDI1MVRTJTJGMjAyNDA0MDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNDA1VDIxMDEzM1omWC1BbXotRXhwaXJlcz00MzIwMCZYLUFtei1TZWN1cml0eS1Ub2tlbj1leUpoYkdjaU9pSklVelV4TWlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKaFkyTmxjM05MWlhraU9pSlZRazh4VERGRE4xVTROMUF4UWxBeU5URlVVeUlzSW1WNGNDSTZNVGN4TWpNNU5EQTRPU3dpY0dGeVpXNTBJam9pYldsdWFXOWhaRzFwYmlKOS5WLUtEZ3JMTVVCbG5KSEtYNlZ4SGw5LUFfLVBGRVdvazJkcFRxLTQ2YmxMbUxzdWVUeHNoVmFZNERad0dmb200VFQ1azhwaFVmZ2pjUWFuc25icmtlQSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmdmVyc2lvbklkPW51bGwmWC1BbXotU2lnbmF0dXJlPTA3Y2FkM2ViMmE2NzIyYjViYWVkMDljNmYxZmU0YTcwMWJmMTJmNDhlMTYyOGI5ZDQ1YzAxMWQ1OTU1Njc4NDU",
|
||||
},
|
||||
wantError: swag.String("403 Forbidden"),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
test: "valid url but with invalid schema returns error",
|
||||
args: args{
|
||||
encodedURL: "cG9zdGdyZXM6Ly9wb3N0Z3JlczoxMjM0NTZAMTI3LjAuMC4xOjU0MzIvZHVtbXk", // postgres://postgres:123456@127.0.0.1:5432/dummy
|
||||
|
||||
},
|
||||
wantError: swag.String("unexpected scheme found postgres"),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
test: "invalid url returns error",
|
||||
args: args{
|
||||
encodedURL: "YXNkc2Fkc2Rh", // asdsadsda
|
||||
|
||||
},
|
||||
wantError: swag.String("unexpected scheme found "),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
test: "plain url",
|
||||
args: args{
|
||||
encodedURL: "aHR0cHM6Ly9sb2NhbGhvc3Q6OTAwMC9jZXN0ZXN0L0F1ZGlvJTIwaWNvbi5zdmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTY",
|
||||
},
|
||||
wantError: nil,
|
||||
expected: swag.String("https://localhost:9000/cestest/Audio%20icon.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.test, func(_ *testing.T) {
|
||||
url, err := decodeMinIOStringURL(tt.args.encodedURL)
|
||||
if tt.wantError != nil {
|
||||
if err != nil {
|
||||
if err.Error() != *tt.wantError {
|
||||
t.Errorf("decodeMinIOStringURL() error: `%v`, wantErr: `%s`", err, *tt.wantError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
t.Errorf("decodeMinIOStringURL() error: `%v`, wantErr: `%s`", err, *tt.wantError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("decodeMinIOStringURL() error: `%s`, wantErr: `%v`", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
tAssert.Equal(*tt.expected, *url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
51
api/tls.go
51
api/tls.go
@@ -1,51 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ConsoleTransport struct {
|
||||
Transport http.RoundTripper
|
||||
ClientIP string
|
||||
}
|
||||
|
||||
func (t *ConsoleTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.ClientIP != "" {
|
||||
// Do not set an empty x-forwarded-for
|
||||
req.Header.Add(xForwardedFor, t.ClientIP)
|
||||
}
|
||||
return t.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// PrepareSTSClientTransport :
|
||||
func PrepareSTSClientTransport(clientIP string) *ConsoleTransport {
|
||||
return &ConsoleTransport{
|
||||
Transport: GlobalTransport,
|
||||
ClientIP: clientIP,
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareConsoleHTTPClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole
|
||||
// custom configurations include the use of CA certificates
|
||||
func PrepareConsoleHTTPClient(clientIP string) *http.Client {
|
||||
// Return http client with default configuration
|
||||
return &http.Client{
|
||||
Transport: PrepareSTSClientTransport(clientIP),
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
bucektApi "github.com/minio/console/api/operations/bucket"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
func registerBucketQuotaHandlers(api *operations.ConsoleAPI) {
|
||||
// get bucket quota
|
||||
api.BucketGetBucketQuotaHandler = bucektApi.GetBucketQuotaHandlerFunc(func(params bucektApi.GetBucketQuotaParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := getBucketQuotaResponse(session, params)
|
||||
if err != nil {
|
||||
return bucektApi.NewGetBucketQuotaDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucektApi.NewGetBucketQuotaOK().WithPayload(resp)
|
||||
})
|
||||
}
|
||||
|
||||
func getBucketQuotaResponse(session *models.Principal, params bucektApi.GetBucketQuotaParams) (*models.BucketQuota, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
quota, err := getBucketQuota(ctx, &adminClient, ¶ms.Name)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
func getBucketQuota(ctx context.Context, ac *AdminClient, bucket *string) (*models.BucketQuota, error) {
|
||||
quota, err := ac.getBucketQuota(ctx, *bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &models.BucketQuota{
|
||||
Quota: int64(quota.Quota),
|
||||
Type: string(quota.Type),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,668 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/mc/cmd"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/sse"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
bucketApi "github.com/minio/console/api/operations/bucket"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth/token"
|
||||
"github.com/minio/minio-go/v7/pkg/policy"
|
||||
minioIAMPolicy "github.com/minio/pkg/v3/policy"
|
||||
)
|
||||
|
||||
func registerBucketsHandlers(api *operations.ConsoleAPI) {
|
||||
// list buckets
|
||||
api.BucketListBucketsHandler = bucketApi.ListBucketsHandlerFunc(func(params bucketApi.ListBucketsParams, session *models.Principal) middleware.Responder {
|
||||
listBucketsResponse, err := getListBucketsResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewListBucketsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewListBucketsOK().WithPayload(listBucketsResponse)
|
||||
})
|
||||
// make bucket
|
||||
api.BucketMakeBucketHandler = bucketApi.MakeBucketHandlerFunc(func(params bucketApi.MakeBucketParams, session *models.Principal) middleware.Responder {
|
||||
makeBucketResponse, err := getMakeBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewMakeBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewMakeBucketOK().WithPayload(makeBucketResponse)
|
||||
})
|
||||
// get bucket info
|
||||
api.BucketBucketInfoHandler = bucketApi.BucketInfoHandlerFunc(func(params bucketApi.BucketInfoParams, session *models.Principal) middleware.Responder {
|
||||
bucketInfoResp, err := getBucketInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewBucketInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return bucketApi.NewBucketInfoOK().WithPayload(bucketInfoResp)
|
||||
})
|
||||
// set bucket tags
|
||||
api.BucketPutBucketTagsHandler = bucketApi.PutBucketTagsHandlerFunc(func(params bucketApi.PutBucketTagsParams, session *models.Principal) middleware.Responder {
|
||||
err := getPutBucketTagsResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewPutBucketTagsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewPutBucketTagsOK()
|
||||
})
|
||||
// get bucket versioning
|
||||
api.BucketGetBucketVersioningHandler = bucketApi.GetBucketVersioningHandlerFunc(func(params bucketApi.GetBucketVersioningParams, session *models.Principal) middleware.Responder {
|
||||
getBucketVersioning, err := getBucketVersionedResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewGetBucketVersioningDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewGetBucketVersioningOK().WithPayload(getBucketVersioning)
|
||||
})
|
||||
// update bucket versioning
|
||||
api.BucketSetBucketVersioningHandler = bucketApi.SetBucketVersioningHandlerFunc(func(params bucketApi.SetBucketVersioningParams, session *models.Principal) middleware.Responder {
|
||||
err := setBucketVersioningResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewSetBucketVersioningDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewSetBucketVersioningCreated()
|
||||
})
|
||||
// get objects rewind for a bucket
|
||||
api.BucketGetBucketRewindHandler = bucketApi.GetBucketRewindHandlerFunc(func(params bucketApi.GetBucketRewindParams, session *models.Principal) middleware.Responder {
|
||||
getBucketRewind, err := getBucketRewindResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewGetBucketRewindDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewGetBucketRewindOK().WithPayload(getBucketRewind)
|
||||
})
|
||||
// get max allowed share link expiration time
|
||||
api.BucketGetMaxShareLinkExpHandler = bucketApi.GetMaxShareLinkExpHandlerFunc(func(params bucketApi.GetMaxShareLinkExpParams, session *models.Principal) middleware.Responder {
|
||||
val, err := getMaxShareLinkExpirationResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewGetMaxShareLinkExpDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewGetMaxShareLinkExpOK().WithPayload(val)
|
||||
})
|
||||
}
|
||||
|
||||
type VersionState string
|
||||
|
||||
const (
|
||||
VersionEnable VersionState = "enable"
|
||||
VersionSuspend VersionState = "suspend"
|
||||
)
|
||||
|
||||
// removeBucket deletes a bucket
|
||||
func doSetVersioning(ctx context.Context, client MCClient, state VersionState, excludePrefix []string, excludeFolders bool) error {
|
||||
err := client.setVersioning(ctx, string(state), excludePrefix, excludeFolders)
|
||||
if err != nil {
|
||||
return err.Cause
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBucketVersioningResponse(session *models.Principal, params bucketApi.SetBucketVersioningParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
bucketName := params.BucketName
|
||||
s3Client, err := newS3BucketClient(session, bucketName, "", getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
amcClient := mcClient{client: s3Client}
|
||||
|
||||
versioningState := VersionSuspend
|
||||
|
||||
if params.Body.Enabled {
|
||||
versioningState = VersionEnable
|
||||
}
|
||||
|
||||
var excludePrefixes []string
|
||||
|
||||
if params.Body.ExcludePrefixes != nil {
|
||||
excludePrefixes = params.Body.ExcludePrefixes
|
||||
}
|
||||
|
||||
excludeFolders := params.Body.ExcludeFolders
|
||||
|
||||
if err := doSetVersioning(ctx, amcClient, versioningState, excludePrefixes, excludeFolders); err != nil {
|
||||
return ErrorWithContext(ctx, fmt.Errorf("error setting versioning for bucket: %s", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBucketVersionedResponse(session *models.Principal, params bucketApi.GetBucketVersioningParams) (*models.BucketVersioningResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minioClient := minioClient{client: mClient}
|
||||
|
||||
// we will tolerate this call failing
|
||||
res, err := minioClient.getBucketVersioning(ctx, params.BucketName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
|
||||
}
|
||||
|
||||
excludedPrefixes := make([]*models.BucketVersioningResponseExcludedPrefixesItems0, len(res.ExcludedPrefixes))
|
||||
for i, v := range res.ExcludedPrefixes {
|
||||
excludedPrefixes[i] = &models.BucketVersioningResponseExcludedPrefixesItems0{
|
||||
Prefix: v.Prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// serialize output
|
||||
bucketVResponse := &models.BucketVersioningResponse{
|
||||
ExcludeFolders: res.ExcludeFolders,
|
||||
ExcludedPrefixes: excludedPrefixes,
|
||||
MFADelete: res.MFADelete,
|
||||
Status: res.Status,
|
||||
}
|
||||
return bucketVResponse, nil
|
||||
}
|
||||
|
||||
// getAccountBuckets fetches a list of all buckets allowed to that particular client from MinIO Servers
|
||||
func getAccountBuckets(ctx context.Context, client MinioAdmin) ([]*models.Bucket, error) {
|
||||
info, err := client.AccountInfo(ctx)
|
||||
if err != nil {
|
||||
return []*models.Bucket{}, err
|
||||
}
|
||||
bucketInfos := []*models.Bucket{}
|
||||
for _, bucket := range info.Buckets {
|
||||
bucketElem := &models.Bucket{
|
||||
CreationDate: bucket.Created.Format(time.RFC3339),
|
||||
Details: &models.BucketDetails{
|
||||
Quota: nil,
|
||||
},
|
||||
RwAccess: &models.BucketRwAccess{
|
||||
Read: bucket.Access.Read,
|
||||
Write: bucket.Access.Write,
|
||||
},
|
||||
Name: swag.String(bucket.Name),
|
||||
Objects: int64(bucket.Objects),
|
||||
Size: int64(bucket.Size),
|
||||
}
|
||||
|
||||
if bucket.Details != nil {
|
||||
if bucket.Details.Tagging != nil {
|
||||
bucketElem.Details.Tags = bucket.Details.Tagging.ToMap()
|
||||
}
|
||||
|
||||
bucketElem.Details.Locking = bucket.Details.Locking
|
||||
bucketElem.Details.Replication = bucket.Details.Replication
|
||||
bucketElem.Details.Versioning = bucket.Details.Versioning
|
||||
bucketElem.Details.VersioningSuspended = bucket.Details.VersioningSuspended
|
||||
if bucket.Details.Quota != nil {
|
||||
bucketElem.Details.Quota = &models.BucketDetailsQuota{
|
||||
Quota: int64(bucket.Details.Quota.Quota),
|
||||
Type: string(bucket.Details.Quota.Type),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bucketInfos = append(bucketInfos, bucketElem)
|
||||
}
|
||||
return bucketInfos, nil
|
||||
}
|
||||
|
||||
// getListBucketsResponse performs listBuckets() and serializes it to the handler's output
|
||||
func getListBucketsResponse(session *models.Principal, params bucketApi.ListBucketsParams) (*models.ListBucketsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
buckets, err := getAccountBuckets(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// serialize output
|
||||
listBucketsResponse := &models.ListBucketsResponse{
|
||||
Buckets: buckets,
|
||||
Total: int64(len(buckets)),
|
||||
}
|
||||
return listBucketsResponse, nil
|
||||
}
|
||||
|
||||
// makeBucket creates a bucket for an specific minio client
|
||||
func makeBucket(ctx context.Context, client MinioClient, bucketName string, objectLocking bool) error {
|
||||
// creates a new bucket with bucketName with a context to control cancellations and timeouts.
|
||||
return client.makeBucketWithContext(ctx, bucketName, "", objectLocking)
|
||||
}
|
||||
|
||||
// getMakeBucketResponse performs makeBucket() to create a bucket with its access policy
|
||||
func getMakeBucketResponse(session *models.Principal, params bucketApi.MakeBucketParams) (*models.MakeBucketsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// bucket request needed to proceed
|
||||
br := params.Body
|
||||
if br == nil {
|
||||
return nil, ErrorWithContext(ctx, ErrBucketBodyNotInRequest)
|
||||
}
|
||||
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minioClient := minioClient{client: mClient}
|
||||
|
||||
if err := makeBucket(ctx, minioClient, *br.Name, false); err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// make sure to delete bucket if an errors occurs after bucket was created
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error creating bucket: %v", err))
|
||||
if err := removeBucket(minioClient, *br.Name); err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error removing bucket: %v", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &models.MakeBucketsResponse{BucketName: *br.Name}, nil
|
||||
}
|
||||
|
||||
// setBucketAccessPolicy set the access permissions on an existing bucket.
|
||||
func setBucketAccessPolicy(ctx context.Context, client MinioClient, bucketName string, access models.BucketAccess, policyDefinition string) error {
|
||||
if strings.TrimSpace(bucketName) == "" {
|
||||
return fmt.Errorf("error: bucket name not present")
|
||||
}
|
||||
if strings.TrimSpace(string(access)) == "" {
|
||||
return fmt.Errorf("error: bucket access not present")
|
||||
}
|
||||
// Prepare policyJSON corresponding to the access type
|
||||
if access != models.BucketAccessPRIVATE && access != models.BucketAccessPUBLIC && access != models.BucketAccessCUSTOM {
|
||||
return fmt.Errorf("access: `%s` not supported", access)
|
||||
}
|
||||
|
||||
bucketAccessPolicy := policy.BucketAccessPolicy{Version: minioIAMPolicy.DefaultVersion}
|
||||
if access == models.BucketAccessCUSTOM {
|
||||
err := client.setBucketPolicyWithContext(ctx, bucketName, policyDefinition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
bucketPolicy := consoleAccess2policyAccess(access)
|
||||
bucketAccessPolicy.Statements = policy.SetPolicy(bucketAccessPolicy.Statements,
|
||||
bucketPolicy, bucketName, "")
|
||||
policyJSON, err := json.Marshal(bucketAccessPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.setBucketPolicyWithContext(ctx, bucketName, string(policyJSON))
|
||||
}
|
||||
|
||||
// putBucketTags sets tags for a bucket
|
||||
func getPutBucketTagsResponse(session *models.Principal, params bucketApi.PutBucketTagsParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minioClient := minioClient{client: mClient}
|
||||
|
||||
req := params.Body
|
||||
bucketName := params.BucketName
|
||||
|
||||
newTagSet, err := tags.NewTags(req.Tags, true)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
err = minioClient.SetBucketTagging(ctx, bucketName, newTagSet)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeBucket deletes a bucket
|
||||
func removeBucket(client MinioClient, bucketName string) error {
|
||||
return client.removeBucket(context.Background(), bucketName)
|
||||
}
|
||||
|
||||
// getBucketInfo return bucket information including name, policy access, size and creation date
|
||||
func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdmin, bucketName string) (*models.Bucket, error) {
|
||||
var bucketAccess models.BucketAccess
|
||||
policyStr, err := client.getBucketPolicy(context.Background(), bucketName)
|
||||
if err != nil {
|
||||
// we can tolerate this errors
|
||||
ErrorWithContext(ctx, fmt.Errorf("error getting bucket policy: %v", err))
|
||||
}
|
||||
|
||||
if policyStr == "" {
|
||||
bucketAccess = models.BucketAccessPRIVATE
|
||||
} else {
|
||||
var p policy.BucketAccessPolicy
|
||||
if err = json.Unmarshal([]byte(policyStr), &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policyAccess := policy.GetPolicy(p.Statements, bucketName, "")
|
||||
if len(p.Statements) > 0 && policyAccess == policy.BucketPolicyNone {
|
||||
bucketAccess = models.BucketAccessCUSTOM
|
||||
} else {
|
||||
bucketAccess = policyAccess2consoleAccess(policyAccess)
|
||||
}
|
||||
}
|
||||
bucketTags, err := client.GetBucketTagging(ctx, bucketName)
|
||||
if err != nil {
|
||||
// we can tolerate this errors
|
||||
ErrorWithContext(ctx, fmt.Errorf("error getting bucket tags: %v", err))
|
||||
}
|
||||
bucketDetails := &models.BucketDetails{}
|
||||
if bucketTags != nil {
|
||||
bucketDetails.Tags = bucketTags.ToMap()
|
||||
}
|
||||
|
||||
info, err := adminClient.AccountInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bucketInfo madmin.BucketAccessInfo
|
||||
|
||||
for _, bucket := range info.Buckets {
|
||||
if bucket.Name == bucketName {
|
||||
bucketInfo = bucket
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &models.Bucket{
|
||||
Name: &bucketName,
|
||||
Access: &bucketAccess,
|
||||
Definition: policyStr,
|
||||
CreationDate: bucketInfo.Created.Format(time.RFC3339),
|
||||
Size: int64(bucketInfo.Size),
|
||||
Details: bucketDetails,
|
||||
Objects: int64(bucketInfo.Objects),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBucketInfoResponse calls getBucketInfo() to get the bucket's info
|
||||
func getBucketInfoResponse(session *models.Principal, params bucketApi.BucketInfoParams) (*models.Bucket, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minioClient := minioClient{client: mClient}
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
bucket, err := getBucketInfo(ctx, minioClient, adminClient, params.Name)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
// policyAccess2consoleAccess gets the equivalent of policy.BucketPolicy to models.BucketAccess
|
||||
func policyAccess2consoleAccess(bucketPolicy policy.BucketPolicy) (bucketAccess models.BucketAccess) {
|
||||
switch bucketPolicy {
|
||||
case policy.BucketPolicyReadWrite:
|
||||
bucketAccess = models.BucketAccessPUBLIC
|
||||
case policy.BucketPolicyNone:
|
||||
bucketAccess = models.BucketAccessPRIVATE
|
||||
default:
|
||||
bucketAccess = models.BucketAccessCUSTOM
|
||||
}
|
||||
return bucketAccess
|
||||
}
|
||||
|
||||
// consoleAccess2policyAccess gets the equivalent of models.BucketAccess to policy.BucketPolicy
|
||||
func consoleAccess2policyAccess(bucketAccess models.BucketAccess) (bucketPolicy policy.BucketPolicy) {
|
||||
switch bucketAccess {
|
||||
case models.BucketAccessPUBLIC:
|
||||
bucketPolicy = policy.BucketPolicyReadWrite
|
||||
case models.BucketAccessPRIVATE:
|
||||
bucketPolicy = policy.BucketPolicyNone
|
||||
}
|
||||
return bucketPolicy
|
||||
}
|
||||
|
||||
// enableBucketEncryption will enable bucket encryption based on two encryption algorithms, sse-s3 (server side encryption with external KMS) or sse-kms (aws s3 kms key)
|
||||
func enableBucketEncryption(ctx context.Context, client MinioClient, bucketName string, encryptionType models.BucketEncryptionType, kmsKeyID string) error {
|
||||
var config *sse.Configuration
|
||||
switch encryptionType {
|
||||
case models.BucketEncryptionTypeSseDashKms:
|
||||
config = sse.NewConfigurationSSEKMS(kmsKeyID)
|
||||
case models.BucketEncryptionTypeSseDashS3:
|
||||
config = sse.NewConfigurationSSES3()
|
||||
default:
|
||||
return ErrInvalidEncryptionAlgorithm
|
||||
}
|
||||
return client.setBucketEncryption(ctx, bucketName, config)
|
||||
}
|
||||
|
||||
// disableBucketEncryption will disable bucket for the provided bucket name
|
||||
func disableBucketEncryption(ctx context.Context, client MinioClient, bucketName string) error {
|
||||
return client.removeBucketEncryption(ctx, bucketName)
|
||||
}
|
||||
|
||||
func getBucketEncryptionInfo(ctx context.Context, client MinioClient, bucketName string) (*models.BucketEncryptionInfo, error) {
|
||||
bucketInfo, err := client.getBucketEncryption(ctx, bucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(bucketInfo.Rules) == 0 {
|
||||
return nil, ErrDefault
|
||||
}
|
||||
return &models.BucketEncryptionInfo{Algorithm: bucketInfo.Rules[0].Apply.SSEAlgorithm, KmsMasterKeyID: bucketInfo.Rules[0].Apply.KmsMasterKeyID}, nil
|
||||
}
|
||||
|
||||
// setBucketRetentionConfig sets object lock configuration on a bucket
|
||||
func setBucketRetentionConfig(ctx context.Context, client MinioClient, bucketName string, mode models.ObjectRetentionMode, unit models.ObjectRetentionUnit, validity *int32) error {
|
||||
if validity == nil {
|
||||
return errors.New("retention validity can't be nil")
|
||||
}
|
||||
|
||||
var retentionMode minio.RetentionMode
|
||||
switch mode {
|
||||
case models.ObjectRetentionModeGovernance:
|
||||
retentionMode = minio.Governance
|
||||
case models.ObjectRetentionModeCompliance:
|
||||
retentionMode = minio.Compliance
|
||||
default:
|
||||
return errors.New("invalid retention mode")
|
||||
}
|
||||
|
||||
var retentionUnit minio.ValidityUnit
|
||||
switch unit {
|
||||
case models.ObjectRetentionUnitDays:
|
||||
retentionUnit = minio.Days
|
||||
case models.ObjectRetentionUnitYears:
|
||||
retentionUnit = minio.Years
|
||||
default:
|
||||
return errors.New("invalid retention unit")
|
||||
}
|
||||
|
||||
retentionValidity := uint(*validity)
|
||||
return client.setObjectLockConfig(ctx, bucketName, &retentionMode, &retentionValidity, &retentionUnit)
|
||||
}
|
||||
|
||||
func getBucketRetentionConfig(ctx context.Context, client MinioClient, bucketName string) (*models.GetBucketRetentionConfig, error) {
|
||||
m, v, u, err := client.getBucketObjectLockConfig(ctx, bucketName)
|
||||
if err != nil {
|
||||
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
|
||||
if errResp.Code == "ObjectLockConfigurationNotFoundError" {
|
||||
return &models.GetBucketRetentionConfig{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// These values can be empty when all are empty, it means
|
||||
// object was created with object locking enabled but
|
||||
// does not have any default object locking configuration.
|
||||
if m == nil && v == nil && u == nil {
|
||||
return &models.GetBucketRetentionConfig{}, nil
|
||||
}
|
||||
|
||||
var mode models.ObjectRetentionMode
|
||||
var unit models.ObjectRetentionUnit
|
||||
|
||||
if m != nil {
|
||||
switch *m {
|
||||
case minio.Governance:
|
||||
mode = models.ObjectRetentionModeGovernance
|
||||
case minio.Compliance:
|
||||
mode = models.ObjectRetentionModeCompliance
|
||||
default:
|
||||
return nil, errors.New("invalid retention mode")
|
||||
}
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
switch *u {
|
||||
case minio.Days:
|
||||
unit = models.ObjectRetentionUnitDays
|
||||
case minio.Years:
|
||||
unit = models.ObjectRetentionUnitYears
|
||||
default:
|
||||
return nil, errors.New("invalid retention unit")
|
||||
}
|
||||
}
|
||||
|
||||
var validity int32
|
||||
if v != nil {
|
||||
validity = int32(*v)
|
||||
}
|
||||
|
||||
config := &models.GetBucketRetentionConfig{
|
||||
Mode: mode,
|
||||
Unit: unit,
|
||||
Validity: validity,
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getBucketRewindResponse(session *models.Principal, params bucketApi.GetBucketRewindParams) (*models.RewindResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
prefix := ""
|
||||
if params.Prefix != nil {
|
||||
prefix = *params.Prefix
|
||||
}
|
||||
s3Client, err := newS3BucketClient(session, params.BucketName, prefix, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating S3Client: %v", err))
|
||||
}
|
||||
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcClient := mcClient{client: s3Client}
|
||||
|
||||
parsedDate, errDate := time.Parse(time.RFC3339, params.Date)
|
||||
|
||||
if errDate != nil {
|
||||
return nil, ErrorWithContext(ctx, errDate)
|
||||
}
|
||||
|
||||
var rewindItems []*models.RewindItem
|
||||
|
||||
for content := range mcClient.client.List(ctx, cmd.ListOptions{TimeRef: parsedDate, WithDeleteMarkers: true}) {
|
||||
// build object name
|
||||
name := strings.ReplaceAll(content.URL.Path, fmt.Sprintf("/%s/", params.BucketName), "")
|
||||
|
||||
listElement := &models.RewindItem{
|
||||
LastModified: content.Time.Format(time.RFC3339),
|
||||
Size: content.Size,
|
||||
VersionID: content.VersionID,
|
||||
DeleteFlag: content.IsDeleteMarker,
|
||||
Action: "",
|
||||
Name: name,
|
||||
}
|
||||
|
||||
rewindItems = append(rewindItems, listElement)
|
||||
}
|
||||
|
||||
return &models.RewindResponse{
|
||||
Objects: rewindItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getMaxShareLinkExpirationResponse(session *models.Principal, params bucketApi.GetMaxShareLinkExpParams) (*models.MaxShareLinkExpResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
maxShareLinkExpSeconds, err := getMaxShareLinkExpirationSeconds(session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.MaxShareLinkExpResponse{Exp: swag.Int64(maxShareLinkExpSeconds)}, nil
|
||||
}
|
||||
|
||||
// getMaxShareLinkExpirationSeconds returns the max share link expiration time in seconds which is the sts token expiration time
|
||||
func getMaxShareLinkExpirationSeconds(session *models.Principal) (int64, error) {
|
||||
creds := getConsoleCredentialsFromSession(session)
|
||||
val, err := creds.GetWithContext(&credentials.CredContext{Client: http.DefaultClient})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if val.SignerType.IsAnonymous() {
|
||||
return 0, ErrAccessDenied
|
||||
}
|
||||
maxShareLinkExp := token.GetConsoleSTSDuration()
|
||||
|
||||
return int64(maxShareLinkExp.Seconds()), nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,324 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
authApi "github.com/minio/console/api/operations/auth"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth"
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
)
|
||||
|
||||
func registerLoginHandlers(api *operations.ConsoleAPI) {
|
||||
// GET login strategy
|
||||
api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder {
|
||||
loginDetails, err := getLoginDetailsResponse(params, GlobalMinIOConfig.OpenIDProviders)
|
||||
if err != nil {
|
||||
return authApi.NewLoginDetailDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return authApi.NewLoginDetailOK().WithPayload(loginDetails)
|
||||
})
|
||||
// POST login using user credentials
|
||||
api.AuthLoginHandler = authApi.LoginHandlerFunc(func(params authApi.LoginParams) middleware.Responder {
|
||||
loginResponse, err := getLoginResponse(params)
|
||||
if err != nil {
|
||||
return authApi.NewLoginDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
// Custom response writer to set the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
|
||||
http.SetCookie(w, &cookie)
|
||||
authApi.NewLoginNoContent().WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
// POST login using external IDP
|
||||
api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOauth2AuthResponse(params, GlobalMinIOConfig.OpenIDProviders)
|
||||
if err != nil {
|
||||
return authApi.NewLoginOauth2AuthDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
// Custom response writer to set the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
|
||||
http.SetCookie(w, &cookie)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Path: "/",
|
||||
Name: "idp-refresh-token",
|
||||
Value: loginResponse.IDPRefreshToken,
|
||||
HttpOnly: true,
|
||||
Secure: len(GlobalPublicCerts) > 0,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
authApi.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// login performs a check of ConsoleCredentials against MinIO, generates some claims and returns the jwt
|
||||
// for subsequent authentication
|
||||
func login(credentials ConsoleCredentialsI, sessionFeatures *auth.SessionFeatures) (*string, error) {
|
||||
// try to obtain consoleCredentials,
|
||||
tokens, err := credentials.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if we made it here, the consoleCredentials work, generate a jwt with claims
|
||||
token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), sessionFeatures)
|
||||
if err != nil {
|
||||
LogError("error authenticating user: %v", err)
|
||||
return nil, ErrInvalidLogin
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// getAccountInfo will return the current user information
|
||||
func getAccountInfo(ctx context.Context, client MinioAdmin) (*madmin.AccountInfo, error) {
|
||||
accountInfo, err := client.AccountInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &accountInfo, nil
|
||||
}
|
||||
|
||||
// getConsoleCredentials will return ConsoleCredentials interface
|
||||
func getConsoleCredentials(accessKey, secretKey string, client *http.Client) (*ConsoleCredentials, error) {
|
||||
creds, err := NewConsoleCredentials(accessKey, secretKey, GetMinIORegion(), client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ConsoleCredentials{
|
||||
ConsoleCredentials: creds,
|
||||
AccountAccessKey: accessKey,
|
||||
CredContext: &credentials.CredContext{
|
||||
Client: client,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getLoginResponse performs login() and serializes it to the handler's output
|
||||
func getLoginResponse(params authApi.LoginParams) (*models.LoginResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
lr := params.Body
|
||||
// trim any leading and trailing whitespace from the login request
|
||||
lr.AccessKey = strings.TrimSpace(lr.AccessKey)
|
||||
lr.SecretKey = strings.TrimSpace(lr.SecretKey)
|
||||
lr.Sts = strings.TrimSpace(lr.Sts)
|
||||
|
||||
clientIP := getClientIP(params.HTTPRequest)
|
||||
client := GetConsoleHTTPClient(clientIP)
|
||||
|
||||
var err error
|
||||
var consoleCreds *ConsoleCredentials
|
||||
// if we receive an STS we use that instead of the credentials
|
||||
if lr.Sts != "" {
|
||||
consoleCreds = &ConsoleCredentials{
|
||||
ConsoleCredentials: credentials.NewStaticV4(lr.AccessKey, lr.SecretKey, lr.Sts),
|
||||
AccountAccessKey: lr.AccessKey,
|
||||
CredContext: &credentials.CredContext{
|
||||
Client: client,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// prepare console credentials
|
||||
consoleCreds, err = getConsoleCredentials(lr.AccessKey, lr.SecretKey, client)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err, ErrInvalidLogin)
|
||||
}
|
||||
}
|
||||
|
||||
sf := &auth.SessionFeatures{}
|
||||
if lr.Features != nil {
|
||||
sf.HideMenu = lr.Features.HideMenu
|
||||
}
|
||||
sessionID, err := login(consoleCreds, sf)
|
||||
if err != nil {
|
||||
if xnet.IsNetworkOrHostDown(err, true) {
|
||||
return nil, ErrorWithContext(ctx, ErrNetworkError)
|
||||
}
|
||||
return nil, ErrorWithContext(ctx, err, ErrInvalidLogin)
|
||||
}
|
||||
// serialize output
|
||||
loginResponse := &models.LoginResponse{
|
||||
SessionID: *sessionID,
|
||||
}
|
||||
return loginResponse, nil
|
||||
}
|
||||
|
||||
// isKubernetes returns true if minio is running in kubernetes.
|
||||
func isKubernetes() bool {
|
||||
// Kubernetes env used to validate if we are
|
||||
// indeed running inside a kubernetes pod
|
||||
// is KUBERNETES_SERVICE_HOST
|
||||
// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L541
|
||||
return env.Get("KUBERNETES_SERVICE_HOST", "") != ""
|
||||
}
|
||||
|
||||
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
|
||||
func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders oauth2.OpenIDPCfg) (ld *models.LoginDetails, apiErr *CodedAPIError) {
|
||||
loginStrategy := models.LoginDetailsLoginStrategyForm
|
||||
var redirectRules []*models.RedirectRule
|
||||
|
||||
r := params.HTTPRequest
|
||||
|
||||
var loginDetails *models.LoginDetails
|
||||
if len(openIDProviders) > 0 {
|
||||
loginStrategy = models.LoginDetailsLoginStrategyRedirect
|
||||
}
|
||||
|
||||
for name, provider := range openIDProviders {
|
||||
// initialize new oauth2 client
|
||||
|
||||
oauth2Client, err := provider.GetOauth2Provider(name, nil, r, GetConsoleHTTPClient(getClientIP(params.HTTPRequest)))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate user against IDP
|
||||
identityProvider := &auth.IdentityProvider{
|
||||
KeyFunc: provider.GetStateKeyFunc(),
|
||||
Client: oauth2Client,
|
||||
}
|
||||
|
||||
displayName := fmt.Sprintf("Login with SSO (%s)", name)
|
||||
serviceType := ""
|
||||
|
||||
if provider.DisplayName != "" {
|
||||
displayName = provider.DisplayName
|
||||
}
|
||||
|
||||
if provider.RoleArn != "" {
|
||||
splitRoleArn := strings.Split(provider.RoleArn, ":")
|
||||
|
||||
if len(splitRoleArn) > 2 {
|
||||
serviceType = splitRoleArn[2]
|
||||
}
|
||||
}
|
||||
|
||||
redirectRule := models.RedirectRule{
|
||||
Redirect: identityProvider.GenerateLoginURL(),
|
||||
DisplayName: displayName,
|
||||
ServiceType: serviceType,
|
||||
}
|
||||
|
||||
redirectRules = append(redirectRules, &redirectRule)
|
||||
}
|
||||
|
||||
if len(openIDProviders) > 0 && len(redirectRules) == 0 {
|
||||
loginStrategy = models.LoginDetailsLoginStrategyForm
|
||||
// No IDP configured fallback to username/password
|
||||
}
|
||||
|
||||
loginDetails = &models.LoginDetails{
|
||||
LoginStrategy: loginStrategy,
|
||||
RedirectRules: redirectRules,
|
||||
IsK8S: isKubernetes(),
|
||||
AnimatedLogin: getConsoleAnimatedLogin(),
|
||||
}
|
||||
|
||||
return loginDetails, nil
|
||||
}
|
||||
|
||||
// verifyUserAgainstIDP will verify user identity against the configured IDP and return MinIO credentials
|
||||
func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, code, state string) (*credentials.Credentials, error) {
|
||||
userCredentials, err := provider.VerifyIdentity(ctx, code, state)
|
||||
if err != nil {
|
||||
LogError("error validating user identity against idp: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return userCredentials, nil
|
||||
}
|
||||
|
||||
func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProviders oauth2.OpenIDPCfg) (*models.LoginResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
r := params.HTTPRequest
|
||||
lr := params.Body
|
||||
|
||||
client := GetConsoleHTTPClient(getClientIP(params.HTTPRequest))
|
||||
if len(openIDProviders) > 0 {
|
||||
// we read state
|
||||
rState := *lr.State
|
||||
|
||||
decodedRState, err := base64.StdEncoding.DecodeString(rState)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
var requestItems oauth2.LoginURLParams
|
||||
if err = json.Unmarshal(decodedRState, &requestItems); err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
IDPName := requestItems.IDPName
|
||||
state := requestItems.State
|
||||
|
||||
providerCfg, ok := openIDProviders[IDPName]
|
||||
if !ok {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("selected IDP %s does not exist", IDPName))
|
||||
}
|
||||
|
||||
// Initialize new identity provider with new oauth2Client per IDPName
|
||||
oauth2Client, err := providerCfg.GetOauth2Provider(IDPName, nil, r, client)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
identityProvider := auth.IdentityProvider{
|
||||
KeyFunc: providerCfg.GetStateKeyFunc(),
|
||||
Client: oauth2Client,
|
||||
RoleARN: providerCfg.RoleArn,
|
||||
}
|
||||
// Validate user against IDP
|
||||
userCredentials, err := verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, state)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// initialize admin client
|
||||
// login user against console and generate session token
|
||||
token, err := login(&ConsoleCredentials{
|
||||
ConsoleCredentials: userCredentials,
|
||||
AccountAccessKey: "",
|
||||
CredContext: &credentials.CredContext{Client: client},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// serialize output
|
||||
loginResponse := &models.LoginResponse{
|
||||
SessionID: *token,
|
||||
IDPRefreshToken: identityProvider.Client.RefreshToken,
|
||||
}
|
||||
return loginResponse, nil
|
||||
}
|
||||
return nil, ErrorWithContext(ctx, ErrDefault)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
authApi "github.com/minio/console/api/operations/auth"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
)
|
||||
|
||||
func registerLogoutHandlers(api *operations.ConsoleAPI) {
|
||||
// logout from console
|
||||
api.AuthLogoutHandler = authApi.LogoutHandlerFunc(func(params authApi.LogoutParams, session *models.Principal) middleware.Responder {
|
||||
err := getLogoutResponse(session, params)
|
||||
if err != nil {
|
||||
api.Logger("IDP logout failed: %v", err.APIError.DetailedMessage)
|
||||
}
|
||||
// Custom response writer to expire the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
if err != nil {
|
||||
w.Header().Set("IDP-Logout", fmt.Sprintf("%v", err.APIError.DetailedMessage))
|
||||
}
|
||||
expiredCookie := ExpireSessionCookie()
|
||||
// this will tell the browser to clear the cookie and invalidate user session
|
||||
// additionally we are deleting the cookie from the client side
|
||||
http.SetCookie(w, &expiredCookie)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Path: "/",
|
||||
Name: "idp-refresh-token",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Expires: time.Now().Add(-100 * time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: len(GlobalPublicCerts) > 0,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
authApi.NewLogoutOK().WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// logout() call Expire() on the provided ConsoleCredentials
|
||||
func logout(credentials ConsoleCredentialsI) {
|
||||
credentials.Expire()
|
||||
}
|
||||
|
||||
// getLogoutResponse performs logout() and returns nil or errors
|
||||
func getLogoutResponse(session *models.Principal, params authApi.LogoutParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
state := params.Body.State
|
||||
if state != "" {
|
||||
if err := logoutFromIDPProvider(params.HTTPRequest, state); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
}
|
||||
creds := getConsoleCredentialsFromSession(session)
|
||||
credentials := ConsoleCredentials{ConsoleCredentials: creds}
|
||||
logout(credentials)
|
||||
return nil
|
||||
}
|
||||
|
||||
func logoutFromIDPProvider(r *http.Request, state string) error {
|
||||
decodedRState, err := base64.StdEncoding.DecodeString(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var requestItems oauth2.LoginURLParams
|
||||
err = json.Unmarshal(decodedRState, &requestItems)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerCfg := GlobalMinIOConfig.OpenIDProviders[requestItems.IDPName]
|
||||
refreshToken, err := r.Cookie("idp-refresh-token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if providerCfg.EndSessionEndpoint != "" {
|
||||
params := url.Values{}
|
||||
params.Add("client_id", providerCfg.ClientID)
|
||||
params.Add("client_secret", providerCfg.ClientSecret)
|
||||
params.Add("refresh_token", refreshToken.Value)
|
||||
client := &http.Client{
|
||||
Transport: GlobalTransport,
|
||||
}
|
||||
result, err := client.PostForm(providerCfg.EndSessionEndpoint, params)
|
||||
if err != nil {
|
||||
return errors.New(500, "failed to logout: %v", err.Error())
|
||||
}
|
||||
if result.StatusCode != 204 {
|
||||
return errors.New(int32(result.StatusCode), "failed to logout")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1257
api/user_objects.go
1257
api/user_objects.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,287 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
policies "github.com/minio/console/api/policy"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/minio/pkg/v3/policy/condition"
|
||||
|
||||
minioIAMPolicy "github.com/minio/pkg/v3/policy"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
authApi "github.com/minio/console/api/operations/auth"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
"github.com/minio/console/pkg/auth/ldap"
|
||||
)
|
||||
|
||||
type Conditions struct {
|
||||
S3Prefix []string `json:"s3:prefix"`
|
||||
}
|
||||
|
||||
func registerSessionHandlers(api *operations.ConsoleAPI) {
|
||||
// session check
|
||||
api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder {
|
||||
sessionResp, err := getSessionResponse(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return authApi.NewSessionCheckDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return authApi.NewSessionCheckOK().WithPayload(sessionResp)
|
||||
})
|
||||
}
|
||||
|
||||
func getClaimsFromToken(sessionToken string) (map[string]interface{}, error) {
|
||||
jp := jwtgo.NewParser()
|
||||
var claims jwtgo.MapClaims
|
||||
_, _, err := jp.ParseUnverified(sessionToken, &claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI
|
||||
func getSessionResponse(ctx context.Context, session *models.Principal) (*models.SessionResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// serialize output
|
||||
if session == nil {
|
||||
return nil, ErrorWithContext(ctx, ErrInvalidSession)
|
||||
}
|
||||
tokenClaims, _ := getClaimsFromToken(session.STSSessionToken)
|
||||
// initialize admin client
|
||||
mAdminClient, err := NewMinioAdminClient(ctx, &models.Principal{
|
||||
STSAccessKeyID: session.STSAccessKeyID,
|
||||
STSSecretAccessKey: session.STSSecretAccessKey,
|
||||
STSSessionToken: session.STSSessionToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
|
||||
}
|
||||
userAdminClient := AdminClient{Client: mAdminClient}
|
||||
// Obtain the current policy assigned to this user
|
||||
// necessary for generating the list of allowed endpoints
|
||||
accountInfo, err := getAccountInfo(ctx, userAdminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
|
||||
}
|
||||
erasure := accountInfo.Server.Type == madmin.Erasure
|
||||
rawPolicy := policies.ReplacePolicyVariables(tokenClaims, accountInfo)
|
||||
policy, err := minioIAMPolicy.ParseConfig(bytes.NewReader(rawPolicy))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
|
||||
}
|
||||
currTime := time.Now().UTC()
|
||||
|
||||
customStyles := session.CustomStyleOb
|
||||
// This actions will be global, meaning has to be attached to all resources
|
||||
conditionValues := map[string][]string{
|
||||
condition.AWSUsername.Name(): {session.AccountAccessKey},
|
||||
// All calls to MinIO from console use temporary credentials.
|
||||
condition.AWSPrincipalType.Name(): {"AssumeRole"},
|
||||
condition.AWSSecureTransport.Name(): {strconv.FormatBool(getMinIOEndpointIsSecure())},
|
||||
condition.AWSCurrentTime.Name(): {currTime.Format(time.RFC3339)},
|
||||
condition.AWSEpochTime.Name(): {strconv.FormatInt(currTime.Unix(), 10)},
|
||||
|
||||
// All calls from console are signature v4.
|
||||
condition.S3SignatureVersion.Name(): {"AWS4-HMAC-SHA256"},
|
||||
// All calls from console use header-based authentication
|
||||
condition.S3AuthType.Name(): {"REST-HEADER"},
|
||||
// This is usually empty, may be set some times (rare).
|
||||
condition.S3LocationConstraint.Name(): {GetMinIORegion()},
|
||||
}
|
||||
|
||||
claims, err := getClaimsFromToken(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
|
||||
}
|
||||
|
||||
// Support all LDAP, JWT variables
|
||||
for k, v := range claims {
|
||||
vstr, ok := v.(string)
|
||||
if !ok {
|
||||
// skip all non-strings
|
||||
continue
|
||||
}
|
||||
// store all claims from sessionToken
|
||||
conditionValues[k] = []string{vstr}
|
||||
}
|
||||
|
||||
defaultActions := policy.IsAllowedActions("", "", conditionValues)
|
||||
|
||||
// Allow Create Access Key when admin:CreateServiceAccount is provided with a condition
|
||||
for _, statement := range policy.Statements {
|
||||
if statement.Effect == "Deny" && len(statement.Conditions) > 0 &&
|
||||
statement.Actions.Contains(minioIAMPolicy.CreateServiceAccountAdminAction) {
|
||||
defaultActions.Add(minioIAMPolicy.Action(minioIAMPolicy.CreateServiceAccountAdminAction))
|
||||
}
|
||||
}
|
||||
|
||||
permissions := map[string]minioIAMPolicy.ActionSet{
|
||||
ConsoleResourceName: defaultActions,
|
||||
}
|
||||
deniedActions := map[string]minioIAMPolicy.ActionSet{}
|
||||
|
||||
var allowResources []*models.PermissionResource
|
||||
|
||||
for _, statement := range policy.Statements {
|
||||
for _, resource := range statement.Resources.ToSlice() {
|
||||
resourceName := resource.String()
|
||||
statementActions := statement.Actions.ToSlice()
|
||||
var prefixes []string
|
||||
|
||||
if statement.Effect == "Allow" {
|
||||
// check if val are denied before adding them to the map
|
||||
var allowedActions []minioIAMPolicy.Action
|
||||
if dActions, ok := deniedActions[resourceName]; ok {
|
||||
for _, action := range statementActions {
|
||||
if len(dActions.Intersection(minioIAMPolicy.NewActionSet(action))) == 0 {
|
||||
// It's ok to allow this action
|
||||
allowedActions = append(allowedActions, action)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allowedActions = statementActions
|
||||
}
|
||||
|
||||
// Add validated actions
|
||||
if resourceActions, ok := permissions[resourceName]; ok {
|
||||
mergedActions := append(resourceActions.ToSlice(), allowedActions...)
|
||||
permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
|
||||
} else {
|
||||
mergedActions := append(defaultActions.ToSlice(), allowedActions...)
|
||||
permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
|
||||
}
|
||||
|
||||
// Allow Permissions request
|
||||
conditions, err := statement.Conditions.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
var wrapper map[string]Conditions
|
||||
|
||||
if err := json.Unmarshal(conditions, &wrapper); err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
for condition, elements := range wrapper {
|
||||
prefixes = elements.S3Prefix
|
||||
|
||||
resourceElement := models.PermissionResource{
|
||||
Resource: resourceName,
|
||||
Prefixes: prefixes,
|
||||
ConditionOperator: condition,
|
||||
}
|
||||
|
||||
allowResources = append(allowResources, &resourceElement)
|
||||
}
|
||||
} else {
|
||||
// Add new banned actions to the map
|
||||
if resourceActions, ok := deniedActions[resourceName]; ok {
|
||||
mergedActions := append(resourceActions.ToSlice(), statementActions...)
|
||||
deniedActions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
|
||||
} else {
|
||||
deniedActions[resourceName] = statement.Actions
|
||||
}
|
||||
// Remove existing val from key if necessary
|
||||
if currentResourceActions, ok := permissions[resourceName]; ok {
|
||||
var newAllowedActions []minioIAMPolicy.Action
|
||||
for _, action := range currentResourceActions.ToSlice() {
|
||||
if len(deniedActions[resourceName].Intersection(minioIAMPolicy.NewActionSet(action))) == 0 {
|
||||
// It's ok to allow this action
|
||||
newAllowedActions = append(newAllowedActions, action)
|
||||
}
|
||||
}
|
||||
permissions[resourceName] = minioIAMPolicy.NewActionSet(newAllowedActions...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resourcePermissions := map[string][]string{}
|
||||
for key, val := range permissions {
|
||||
var resourceActions []string
|
||||
for _, action := range val.ToSlice() {
|
||||
resourceActions = append(resourceActions, string(action))
|
||||
}
|
||||
resourcePermissions[key] = resourceActions
|
||||
|
||||
}
|
||||
|
||||
// environment constants
|
||||
var envConstants models.EnvironmentConstants
|
||||
|
||||
envConstants.MaxConcurrentUploads = getMaxConcurrentUploadsLimit()
|
||||
envConstants.MaxConcurrentDownloads = getMaxConcurrentDownloadsLimit()
|
||||
|
||||
sessionResp := &models.SessionResponse{
|
||||
Features: getListOfEnabledFeatures(ctx, userAdminClient, session),
|
||||
Status: models.SessionResponseStatusOk,
|
||||
Operator: false,
|
||||
DistributedMode: erasure,
|
||||
Permissions: resourcePermissions,
|
||||
AllowResources: allowResources,
|
||||
CustomStyles: customStyles,
|
||||
EnvConstants: &envConstants,
|
||||
ServerEndPoint: getMinIOServer(),
|
||||
}
|
||||
return sessionResp, nil
|
||||
}
|
||||
|
||||
// getListOfEnabledFeatures returns a list of features
|
||||
func getListOfEnabledFeatures(ctx context.Context, minioClient MinioAdmin, session *models.Principal) []string {
|
||||
features := []string{}
|
||||
logSearchURL := getLogSearchURL()
|
||||
oidcEnabled := oauth2.IsIDPEnabled()
|
||||
ldapEnabled := ldap.GetLDAPEnabled()
|
||||
|
||||
if logSearchURL != "" {
|
||||
features = append(features, "log-search")
|
||||
}
|
||||
if oidcEnabled {
|
||||
features = append(features, "oidc-idp", "external-idp")
|
||||
}
|
||||
if ldapEnabled {
|
||||
features = append(features, "ldap-idp", "external-idp")
|
||||
}
|
||||
|
||||
if session.Hm {
|
||||
features = append(features, "hide-menu")
|
||||
}
|
||||
if session.Ob {
|
||||
features = append(features, "object-browser-only")
|
||||
}
|
||||
if minioClient != nil {
|
||||
_, err := minioClient.kmsStatus(ctx)
|
||||
if err == nil {
|
||||
features = append(features, "kms")
|
||||
}
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
"github.com/minio/console/pkg/auth/ldap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getSessionResponse(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
session *models.Principal
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *models.SessionResponse
|
||||
wantErr bool
|
||||
preFunc func()
|
||||
postFunc func()
|
||||
}{
|
||||
{
|
||||
name: "empty session",
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
session: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "malformed session",
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
session: &models.Principal{
|
||||
STSAccessKeyID: "W257A03HTI7L30F7YCRD",
|
||||
STSSecretAccessKey: "g+QVorWQR8aSy+k3OHOoYn0qKpENld72faCMfYps",
|
||||
STSSessionToken: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJXMjU3QTAzSFRJN0wzMEY3WUNSRCIsImV4cCI6MTY1MTAxNjU1OCwicGFyZW50IjoibWluaW8ifQ.uFFIIEQ6qM_QvMM297ODi_uK2IA1pwvsDbyBGErkQKqtbY_Ynte8GUkNsSHBEMCT9Fr7uUwaxK41kUqjtbqAwA",
|
||||
AccountAccessKey: "minio",
|
||||
Hm: false,
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
if tt.preFunc != nil {
|
||||
tt.preFunc()
|
||||
}
|
||||
session, err := getSessionResponse(tt.args.ctx, tt.args.session)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getSessionResponse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(session, tt.want) {
|
||||
t.Errorf("getSessionResponse() got = %v, want %v", session, tt.want)
|
||||
}
|
||||
if tt.postFunc != nil {
|
||||
tt.postFunc()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getListOfEnabledFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
session *models.Principal
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
preFunc func()
|
||||
postFunc func()
|
||||
}{
|
||||
{
|
||||
name: "all features are enabled",
|
||||
args: args{
|
||||
session: &models.Principal{
|
||||
STSAccessKeyID: "",
|
||||
STSSecretAccessKey: "",
|
||||
STSSessionToken: "",
|
||||
AccountAccessKey: "",
|
||||
Hm: true,
|
||||
},
|
||||
},
|
||||
want: []string{"log-search", "oidc-idp", "external-idp", "ldap-idp", "external-idp", "hide-menu"},
|
||||
preFunc: func() {
|
||||
os.Setenv(ConsoleLogQueryURL, "http://logsearchapi:8080")
|
||||
os.Setenv(oauth2.ConsoleIDPURL, "http://external-idp.com")
|
||||
os.Setenv(oauth2.ConsoleIDPClientID, "eaeaeaeaeaea")
|
||||
os.Setenv(ldap.ConsoleLDAPEnabled, "on")
|
||||
},
|
||||
postFunc: func() {
|
||||
os.Unsetenv(ConsoleLogQueryURL)
|
||||
os.Unsetenv(oauth2.ConsoleIDPURL)
|
||||
os.Unsetenv(oauth2.ConsoleIDPClientID)
|
||||
os.Unsetenv(ldap.ConsoleLDAPEnabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
if tt.preFunc != nil {
|
||||
tt.preFunc()
|
||||
}
|
||||
assert.Equalf(t, tt.want, getListOfEnabledFeatures(context.Background(), nil, tt.args.session), "getListOfEnabledFeatures(%v)", tt.args.session)
|
||||
if tt.postFunc != nil {
|
||||
tt.postFunc()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
255
api/utils.go
255
api/utils.go
@@ -1,255 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xjwt "github.com/minio/console/pkg/auth/token"
|
||||
)
|
||||
|
||||
// Do not use:
|
||||
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
|
||||
// for access/secret keys.
|
||||
|
||||
// The alphabet of random character string. Each character must be unique.
|
||||
//
|
||||
// The RandomCharString implementation requires that: 256 / len(letters) is a natural numbers.
|
||||
// For example: 256 / 64 = 4. However, 5 > 256/62 > 4 and therefore we must not use a alphabet
|
||||
// of 62 characters.
|
||||
// The reason is that if 256 / len(letters) is not a natural number then certain characters become
|
||||
// more likely then others.
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
|
||||
|
||||
type CustomButtonStyle struct {
|
||||
BackgroundColor *string `json:"backgroundColor"`
|
||||
TextColor *string `json:"textColor"`
|
||||
HoverColor *string `json:"hoverColor"`
|
||||
HoverText *string `json:"hoverText"`
|
||||
ActiveColor *string `json:"activeColor"`
|
||||
ActiveText *string `json:"activeText"`
|
||||
DisabledColor *string `json:"disabledColor"`
|
||||
DisabledText *string `json:"disdabledText"`
|
||||
}
|
||||
|
||||
type CustomTableStyle struct {
|
||||
Border *string `json:"border"`
|
||||
DisabledBorder *string `json:"disabledBorder"`
|
||||
DisabledBG *string `json:"disabledBG"`
|
||||
Selected *string `json:"selected"`
|
||||
DeletedDisabled *string `json:"deletedDisabled"`
|
||||
HoverColor *string `json:"hoverColor"`
|
||||
}
|
||||
|
||||
type CustomInputStyle struct {
|
||||
Border *string `json:"border"`
|
||||
HoverBorder *string `json:"hoverBorder"`
|
||||
TextColor *string `json:"textColor"`
|
||||
BackgroundColor *string `json:"backgroundColor"`
|
||||
}
|
||||
|
||||
type CustomSwitchStyle struct {
|
||||
SwitchBackground *string `json:"switchBackground"`
|
||||
BulletBorderColor *string `json:"bulletBorderColor"`
|
||||
BulletBGColor *string `json:"bulletBGColor"`
|
||||
DisabledBackground *string `json:"disabledBackground"`
|
||||
DisabledBulletBorderColor *string `json:"disabledBulletBorderColor"`
|
||||
DisabledBulletBGColor *string `json:"disabledBulletBGColor"`
|
||||
}
|
||||
|
||||
type CustomStyles struct {
|
||||
BackgroundColor *string `json:"backgroundColor"`
|
||||
FontColor *string `json:"fontColor"`
|
||||
SecondaryFontColor *string `json:"secondaryFontColor"`
|
||||
BorderColor *string `json:"borderColor"`
|
||||
LoaderColor *string `json:"loaderColor"`
|
||||
BoxBackground *string `json:"boxBackground"`
|
||||
OkColor *string `json:"okColor"`
|
||||
ErrorColor *string `json:"errorColor"`
|
||||
WarnColor *string `json:"warnColor"`
|
||||
LinkColor *string `json:"linkColor"`
|
||||
DisabledLinkColor *string `json:"disabledLinkColor"`
|
||||
HoverLinkColor *string `json:"hoverLinkColor"`
|
||||
ButtonStyles *CustomButtonStyle `json:"buttonStyles"`
|
||||
SecondaryButtonStyles *CustomButtonStyle `json:"secondaryButtonStyles"`
|
||||
RegularButtonStyles *CustomButtonStyle `json:"regularButtonStyles"`
|
||||
TableColors *CustomTableStyle `json:"tableColors"`
|
||||
InputBox *CustomInputStyle `json:"inputBox"`
|
||||
Switch *CustomSwitchStyle `json:"switch"`
|
||||
}
|
||||
|
||||
func RandomCharStringWithAlphabet(n int, alphabet string) string {
|
||||
random := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, random); err != nil {
|
||||
panic(err) // Can only happen if we would run out of entropy.
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
for _, v := range random {
|
||||
j := v % byte(len(alphabet))
|
||||
s.WriteByte(alphabet[j])
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func RandomCharString(n int) string {
|
||||
return RandomCharStringWithAlphabet(n, letters)
|
||||
}
|
||||
|
||||
// DifferenceArrays returns the elements in `a` that aren't in `b`.
|
||||
func DifferenceArrays(a, b []string) []string {
|
||||
mb := make(map[string]struct{}, len(b))
|
||||
for _, x := range b {
|
||||
mb[x] = struct{}{}
|
||||
}
|
||||
var diff []string
|
||||
for _, x := range a {
|
||||
if _, found := mb[x]; !found {
|
||||
diff = append(diff, x)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// IsElementInArray returns true if the string belongs to the slice
|
||||
func IsElementInArray(a []string, b string) bool {
|
||||
for _, e := range a {
|
||||
if e == b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// UniqueKeys returns an array without duplicated keys
|
||||
func UniqueKeys(a []string) []string {
|
||||
keys := make(map[string]bool)
|
||||
list := []string{}
|
||||
for _, entry := range a {
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
list = append(list, entry)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func NewSessionCookieForConsole(token string) http.Cookie {
|
||||
sessionDuration := xjwt.GetConsoleSTSDuration()
|
||||
return http.Cookie{
|
||||
Path: "/",
|
||||
Name: "token",
|
||||
Value: token,
|
||||
MaxAge: int(sessionDuration.Seconds()), // default 1 hr
|
||||
Expires: time.Now().Add(sessionDuration),
|
||||
HttpOnly: true,
|
||||
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
|
||||
// should not leak any cookie if we access the site using HTTP
|
||||
Secure: len(GlobalPublicCerts) > 0,
|
||||
// read more: https://web.dev/samesite-cookies-explained/
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
func ExpireSessionCookie() http.Cookie {
|
||||
return http.Cookie{
|
||||
Path: "/",
|
||||
Name: "token",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Expires: time.Now().Add(-100 * time.Hour),
|
||||
HttpOnly: true,
|
||||
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
|
||||
// should not leak any cookie if we access the site using HTTP
|
||||
Secure: len(GlobalPublicCerts) > 0,
|
||||
// read more: https://web.dev/samesite-cookies-explained/
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateEncodedStyles(encodedStyles string) error {
|
||||
// encodedStyle JSON validation
|
||||
str, err := base64.StdEncoding.DecodeString(encodedStyles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var styleElements *CustomStyles
|
||||
|
||||
err = json.Unmarshal(str, &styleElements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if styleElements.BackgroundColor == nil || styleElements.FontColor == nil || styleElements.ButtonStyles == nil || styleElements.BorderColor == nil || styleElements.OkColor == nil {
|
||||
return errors.New("specified style is not in the correct format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var safeMimeTypes = []string{
|
||||
"image/jpeg",
|
||||
"image/apng",
|
||||
"image/avif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/x-icon",
|
||||
"image/gif",
|
||||
"image/png",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"application/pdf",
|
||||
"text/plain",
|
||||
"application/json",
|
||||
"audio/wav",
|
||||
"audio/mpeg",
|
||||
"audio/aiff",
|
||||
"audio/dsd",
|
||||
"video/mp4",
|
||||
"video/x-msvideo",
|
||||
"video/mpeg",
|
||||
"audio/webm",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
"video/x-flv",
|
||||
"audio/x-matroska",
|
||||
"video/x-matroska",
|
||||
"video/x-ms-wmv",
|
||||
"application/metastream",
|
||||
"video/avchd-stream",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
}
|
||||
|
||||
func isSafeToPreview(str string) bool {
|
||||
for _, v := range safeMimeTypes {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDifferenceArrays(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
exampleArrayAMock := []string{"a", "b", "c"}
|
||||
exampleArrayBMock := []string{"b", "d"}
|
||||
resultABArrayMock := []string{"a", "c"}
|
||||
resultBAArrayMock := []string{"d"}
|
||||
|
||||
// Test-1: test DifferenceArrays() with array a vs array b
|
||||
diffArray := DifferenceArrays(exampleArrayAMock, exampleArrayBMock)
|
||||
assert.ElementsMatchf(diffArray, resultABArrayMock, "return array AB doesn't match %s")
|
||||
|
||||
// Test-2: test DifferenceArrays() with array b vs array a
|
||||
diffArray2 := DifferenceArrays(exampleArrayBMock, exampleArrayAMock)
|
||||
assert.ElementsMatchf(diffArray2, resultBAArrayMock, "return array BA doesn't match %s")
|
||||
}
|
||||
|
||||
func TestIsElementInArray(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
exampleElementsArray := []string{"c", "a", "d", "b"}
|
||||
|
||||
// Test-1: test IsElementInArray() with element that is in the list
|
||||
responseArray := IsElementInArray(exampleElementsArray, "a")
|
||||
assert.Equal(true, responseArray)
|
||||
|
||||
// Test-2: test IsElementInArray() with element that is not in the list
|
||||
responseArray2 := IsElementInArray(exampleElementsArray, "e")
|
||||
assert.Equal(false, responseArray2)
|
||||
}
|
||||
|
||||
func TestUniqueKeys(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
exampleMixedArray := []string{"a", "b", "c", "e", "d", "b", "c", "h", "f", "g"}
|
||||
exampleUniqueArray := []string{"a", "b", "c", "e", "d", "h", "f", "g"}
|
||||
|
||||
// Test-1 test UniqueKeys returns an array with unique elements
|
||||
responseArray := UniqueKeys(exampleMixedArray)
|
||||
assert.ElementsMatchf(responseArray, exampleUniqueArray, "returned array doesn't contain the correct elements %s")
|
||||
}
|
||||
|
||||
func TestRandomCharStringWithAlphabet(t *testing.T) {
|
||||
type args struct {
|
||||
n int
|
||||
alphabet string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "generated random string has the right length",
|
||||
args: args{
|
||||
n: 10,
|
||||
alphabet: "A",
|
||||
},
|
||||
want: "AAAAAAAAAA",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.want, RandomCharStringWithAlphabet(tt.args.n, tt.args.alphabet), "RandomCharStringWithAlphabet(%v, %v)", tt.args.n, tt.args.alphabet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSessionCookieForConsole(t *testing.T) {
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want http.Cookie
|
||||
}{
|
||||
{
|
||||
name: "session cookie has the right token an security configuration",
|
||||
args: args{
|
||||
token: "jwt-xxxxxxxxx",
|
||||
},
|
||||
want: http.Cookie{
|
||||
Path: "/",
|
||||
Value: "jwt-xxxxxxxxx",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Name: "token",
|
||||
MaxAge: 43200,
|
||||
Expires: time.Now().Add(1 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got := NewSessionCookieForConsole(tt.args.token)
|
||||
assert.Equalf(t, tt.want.Value, got.Value, "NewSessionCookieForConsole(%v)", tt.args.token)
|
||||
assert.Equalf(t, tt.want.Path, got.Path, "NewSessionCookieForConsole(%v)", tt.args.token)
|
||||
assert.Equalf(t, tt.want.HttpOnly, got.HttpOnly, "NewSessionCookieForConsole(%v)", tt.args.token)
|
||||
assert.Equalf(t, tt.want.Name, got.Name, "NewSessionCookieForConsole(%v)", tt.args.token)
|
||||
assert.Equalf(t, tt.want.MaxAge, got.MaxAge, "NewSessionCookieForConsole(%v)", tt.args.token)
|
||||
assert.Equalf(t, tt.want.SameSite, got.SameSite, "NewSessionCookieForConsole(%v)", tt.args.token)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpireSessionCookie(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want http.Cookie
|
||||
}{
|
||||
{
|
||||
name: "cookie is expired correctly",
|
||||
want: http.Cookie{
|
||||
Name: "token",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got := ExpireSessionCookie()
|
||||
assert.Equalf(t, tt.want.Name, got.Name, "ExpireSessionCookie()")
|
||||
assert.Equalf(t, tt.want.Value, got.Value, "ExpireSessionCookie()")
|
||||
assert.Equalf(t, tt.want.MaxAge, got.MaxAge, "ExpireSessionCookie()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isSafeToPreview(t *testing.T) {
|
||||
type args struct {
|
||||
str string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "mime type is safe to preview",
|
||||
args: args{
|
||||
str: "image/jpeg",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mime type is not safe to preview",
|
||||
args: args{
|
||||
str: "application/zip",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.want, isSafeToPreview(tt.args.str), "isSafeToPreview(%v)", tt.args.str)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomCharString(t *testing.T) {
|
||||
type args struct {
|
||||
n int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLength int
|
||||
}{
|
||||
{
|
||||
name: "valid string",
|
||||
args: args{
|
||||
n: 1,
|
||||
},
|
||||
wantLength: 1,
|
||||
}, {
|
||||
name: "valid string",
|
||||
args: args{
|
||||
n: 64,
|
||||
},
|
||||
wantLength: 64,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.wantLength, len(RandomCharString(tt.args.n)), "RandomCharString(%v)", tt.args.n)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEncodedStyles(t *testing.T) {
|
||||
type args struct {
|
||||
encodedStyles string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
encodedStyles: "ewogICJiYWNrZ3JvdW5kQ29sb3IiOiAiIzFjMWMxYyIsCiAgImZvbnRDb2xvciI6ICJ3aGl0ZSIsCiAgInNlY29uZGFyeUZvbnRDb2xvciI6ICJncmV5IiwKICAiYm9yZGVyQ29sb3IiOiAieWVsbG93IiwKICAibG9hZGVyQ29sb3IiOiAicmVkIiwKICAiYm94QmFja2dyb3VuZCI6ICIjMDU3OWFmIiwKICAib2tDb2xvciI6ICIjMDhhZjA1IiwKICAiZXJyb3JDb2xvciI6ICIjYmYxZTQ2IiwKICAid2FybkNvbG9yIjogIiNiZmFjMWUiLAogICJsaW5rQ29sb3IiOiAiIzFlYmZiZiIsCiAgImRpc2FibGVkTGlua0NvbG9yIjogIiM5ZGEwYTAiLAogICJob3ZlckxpbmtDb2xvciI6ICIjMGY0ZWJjIiwKICAidGFibGVDb2xvcnMiOiB7CiAgICAiYm9yZGVyIjogIiM0YmJjMGYiLAogICAgImRpc2FibGVkQm9yZGVyIjogIiM3MjhlNjMiLAogICAgImRpc2FibGVkQkciOiAiIzcyOGU2MyIsCiAgICAic2VsZWN0ZWQiOiAiIzU1ZGIwZCIsCiAgICAiZGVsZXRlZERpc2FibGVkIjogIiNlYWI2ZDAiLAogICAgImhvdmVyQ29sb3IiOiAiIzAwZmZmNiIKICB9LAogICJidXR0b25TdHlsZXMiOiB7CiAgICAiYmFja2dyb3VuZENvbG9yIjogIiMwMDczZmYiLAogICAgInRleHRDb2xvciI6ICIjZmZmZmZmIiwKICAgICJob3ZlckNvbG9yIjogIiMyYjhlZmYiLAogICAgImhvdmVyVGV4dCI6ICIjZmZmIiwKICAgICJhY3RpdmVDb2xvciI6ICIjMzg4M2Q4IiwKICAgICJhY3RpdmVUZXh0IjogIiNmZmYiLAogICAgImRpc2FibGVkQ29sb3IiOiAiIzc1OGU4ZCIsCiAgICAiZGlzYWJsZWRUZXh0IjogIiNkOWRkZGQiCiAgfSwKICAic2Vjb25kYXJ5QnV0dG9uU3R5bGVzIjogewogICAgImJhY2tncm91bmRDb2xvciI6ICIjZWEzMzc5IiwKICAgICJ0ZXh0Q29sb3IiOiAiI2VhMzM3OSIsCiAgICAiaG92ZXJDb2xvciI6ICIjZWEzMzAwIiwKICAgICJob3ZlclRleHQiOiAiI2VhMzMwMCIsCiAgICAiYWN0aXZlQ29sb3IiOiAiI2VhMzM3OSIsCiAgICAiYWN0aXZlVGV4dCI6ICIjM2NlYTMzIiwKICAgICJkaXNhYmxlZENvbG9yIjogIiM3ODdjNzciLAogICAgImRpc2FibGVkVGV4dCI6ICIjNzg3Yzc3IgogIH0sCiAgInJlZ3VsYXJCdXR0b25TdHlsZXMiOiB7CiAgICAiYmFja2dyb3VuZENvbG9yIjogIiMwMDczZmYiLAogICAgInRleHRDb2xvciI6ICIjMDA3M2ZmIiwKICAgICJob3ZlckNvbG9yIjogIiMyYjhlZmYiLAogICAgImhvdmVyVGV4dCI6ICIjMDA3M2ZmIiwKICAgICJhY3RpdmVDb2xvciI6ICIjMzg4M2Q4IiwKICAgICJhY3RpdmVUZXh0IjogIiMwMDczZmYiLAogICAgImRpc2FibGVkQ29sb3IiOiAiIzc1OGU4ZCIsCiAgICAiZGlzYWJsZWRUZXh0IjogIiNkOWRkZGQiCiAgfSwKICAiaW5wdXRCb3giOiB7CiAgICAiYm9yZGVyIjogImdyZWVuIiwKICAgICJob3ZlckJvcmRlciI6ICJibHVlIiwKICAgICJ0ZXh0Q29sb3IiOiAid2hpdGUiLAogICAgImJhY2tncm91bmRDb2xvciI6ICIjMjhkNGZmIgogIH0sCiAgInN3aXRjaCI6IHsKICAgICJzd2l0Y2hCYWNrZ3JvdW5kIjogIiMyOGQ0ZmYiLAogICAgImJ1bGxldEJvcmRlckNvbG9yIjogIiNhNmFjYWQiLAogICAgImJ1bGxldEJHQ29sb3IiOiAiI2RjZTFlMiIsCiAgICAiZGlzYWJsZWRCYWNrZ3JvdW5kIjogIiM0NzQ5NDkiLAogICAgImRpc2FibGVkQnVsbGV0Qm9yZGVyQ29sb3IiOiAiIzQ3NDk0OSIsCiAgICAiZGlzYWJsZWRCdWxsZXRCR0NvbG9yIjogIiM3Mzk3YTAiCiAgfQp9",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid config",
|
||||
args: args{
|
||||
encodedStyles: "ewogICJvb3JnbGUiOiAic3MiCn0===",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid style config",
|
||||
args: args{
|
||||
encodedStyles: "e30=",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid base64",
|
||||
args: args{
|
||||
encodedStyles: "duck",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
if tt.wantErr {
|
||||
assert.NotNilf(t, ValidateEncodedStyles(tt.args.encodedStyles), "Wanted an error")
|
||||
} else {
|
||||
assert.Nilf(t, ValidateEncodedStyles(tt.args.encodedStyles), "Did not wanted an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
233
api/ws_handle.go
233
api/ws_handle.go
@@ -1,233 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
errorsApi "github.com/go-openapi/errors"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth"
|
||||
"github.com/minio/console/pkg/utils"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 0,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
const (
|
||||
// websocket base path
|
||||
wsBasePath = "/ws"
|
||||
)
|
||||
|
||||
type wsMinioClient struct {
|
||||
// websocket connection.
|
||||
conn wsConn
|
||||
// MinIO admin Client
|
||||
client minioClient
|
||||
}
|
||||
|
||||
// WSConn interface with all functions to be implemented
|
||||
// by mock when testing, it should include all websocket.Conn
|
||||
// respective api calls that are used within this project.
|
||||
type WSConn interface {
|
||||
writeMessage(messageType int, data []byte) error
|
||||
close() error
|
||||
readMessage() (messageType int, p []byte, err error)
|
||||
remoteAddress() string
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
//
|
||||
// Define the structure of a websocket Connection
|
||||
type wsConn struct {
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func (c wsConn) writeMessage(messageType int, data []byte) error {
|
||||
return c.conn.WriteMessage(messageType, data)
|
||||
}
|
||||
|
||||
func (c wsConn) close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c wsConn) readMessage() (messageType int, p []byte, err error) {
|
||||
return c.conn.ReadMessage()
|
||||
}
|
||||
|
||||
func (c wsConn) remoteAddress() string {
|
||||
clientIP, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
// In case there's an error, return an empty string
|
||||
log.Printf("Invalid ws.clientIP = %s\n", err)
|
||||
return ""
|
||||
}
|
||||
return clientIP
|
||||
}
|
||||
|
||||
// serveWS validates the incoming request and
|
||||
// upgrades the request to a Websocket protocol.
|
||||
// Websocket communication will be done depending
|
||||
// on the path.
|
||||
// Request should come like ws://<host>:<port>/ws/<api>
|
||||
func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
|
||||
// Perform authentication before upgrading to a Websocket Connection
|
||||
// authenticate WS connection with Console
|
||||
session, err := auth.GetClaimsFromTokenInRequest(req)
|
||||
if err != nil && (errors.Is(err, auth.ErrReadingToken) && !strings.HasPrefix(wsPath, `/objectManager`)) {
|
||||
ErrorWithContext(ctx, err)
|
||||
errorsApi.ServeError(w, req, errorsApi.New(http.StatusUnauthorized, "%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are using a subpath we are most likely behind a reverse proxy so we most likely
|
||||
// can't validate the proper Origin since we don't know the source domain, so we are going
|
||||
// to allow the connection to be upgraded in this case.
|
||||
if getSubPath() != "/" || getConsoleDevMode() {
|
||||
upgrader.CheckOrigin = func(_ *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// upgrades the HTTP server connection to the WebSocket protocol.
|
||||
conn, err := upgrader.Upgrade(w, req, nil)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, err)
|
||||
errorsApi.ServeError(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := getSourceIPFromHeaders(req)
|
||||
if clientIP == "" {
|
||||
if ip, _, err := net.SplitHostPort(conn.RemoteAddr().String()); err == nil {
|
||||
clientIP = ip
|
||||
} else {
|
||||
// In case there's an error, return an empty string
|
||||
LogError("Invalid ws.RemoteAddr() = %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(wsPath, `/objectManager`):
|
||||
wsMinioClient, err := newWebSocketMinioClient(conn, session, clientIP)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, err)
|
||||
closeWsConn(conn)
|
||||
return
|
||||
}
|
||||
|
||||
go wsMinioClient.objectManager(session)
|
||||
default:
|
||||
// path not found
|
||||
closeWsConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func newWebSocketMinioClient(conn *websocket.Conn, claims *models.Principal, clientIP string) (*wsMinioClient, error) {
|
||||
mClient, err := newMinioClient(claims, clientIP)
|
||||
if err != nil {
|
||||
LogError("error creating MinioClient:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a websocket connection interface implementation
|
||||
// defining the connection to be used
|
||||
wsConnection := wsConn{conn: conn}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minioClient := minioClient{client: mClient}
|
||||
|
||||
// create websocket client and handle request
|
||||
wsMinioClient := &wsMinioClient{conn: wsConnection, client: minioClient}
|
||||
return wsMinioClient, nil
|
||||
}
|
||||
|
||||
// wsReadClientCtx reads the messages that come from the client
|
||||
// if the client sends a Close Message the context will be
|
||||
// canceled. If the connection is closed the goroutine inside
|
||||
// will return.
|
||||
func wsReadClientCtx(parentContext context.Context, conn WSConn) context.Context {
|
||||
// a cancel context is needed to end all goroutines used
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var requestID string
|
||||
var SessionID string
|
||||
var UserAgent string
|
||||
var Host string
|
||||
var RemoteHost string
|
||||
|
||||
if val, o := parentContext.Value(utils.ContextRequestID).(string); o {
|
||||
requestID = val
|
||||
}
|
||||
if val, o := parentContext.Value(utils.ContextRequestUserID).(string); o {
|
||||
SessionID = val
|
||||
}
|
||||
if val, o := parentContext.Value(utils.ContextRequestUserAgent).(string); o {
|
||||
UserAgent = val
|
||||
}
|
||||
if val, o := parentContext.Value(utils.ContextRequestHost).(string); o {
|
||||
Host = val
|
||||
}
|
||||
if val, o := parentContext.Value(utils.ContextRequestRemoteAddr).(string); o {
|
||||
RemoteHost = val
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestID, requestID)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestUserID, SessionID)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, UserAgent)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestHost, Host)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, RemoteHost)
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
for {
|
||||
_, _, err := conn.readMessage()
|
||||
if err != nil {
|
||||
// if errors of type websocket.CloseError and is Unexpected
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error unexpected CloseError on ReadMessage: %v", err))
|
||||
return
|
||||
}
|
||||
// Not all errors are of type websocket.CloseError.
|
||||
if _, ok := err.(*websocket.CloseError); !ok {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error on ReadMessage: %v", err))
|
||||
return
|
||||
}
|
||||
// else is an expected Close Error
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// closeWsConn sends Close Message and closes the websocket connection
|
||||
func closeWsConn(conn *websocket.Conn) {
|
||||
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
conn.Close()
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
func (wsc *wsMinioClient) objectManager(session *models.Principal) {
|
||||
// Storage of Cancel Contexts for this connection
|
||||
var cancelContexts sync.Map
|
||||
// Initial goroutine
|
||||
defer func() {
|
||||
// We close socket at the end of requests
|
||||
wsc.conn.close()
|
||||
cancelContexts.Range(func(key, value interface{}) bool {
|
||||
cancelFunc := value.(context.CancelFunc)
|
||||
cancelFunc()
|
||||
|
||||
cancelContexts.Delete(key)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
writeChannel := make(chan WSResponse)
|
||||
done := make(chan interface{})
|
||||
|
||||
sendWSResponse := func(r WSResponse) {
|
||||
select {
|
||||
case writeChannel <- r:
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
// Read goroutine
|
||||
go func() {
|
||||
defer close(writeChannel)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
mType, message, err := wsc.conn.readMessage()
|
||||
if err != nil {
|
||||
LogInfo("Error while reading objectManager message: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if mType == websocket.TextMessage {
|
||||
// We get request data & review information
|
||||
var messageRequest ObjectsRequest
|
||||
|
||||
if err := json.Unmarshal(message, &messageRequest); err != nil {
|
||||
LogInfo("Error on message request unmarshal: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// new message, new context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// We store the cancel func associated with this request
|
||||
cancelContexts.Store(messageRequest.RequestID, cancel)
|
||||
|
||||
switch messageRequest.Mode {
|
||||
case "objects", "rewind":
|
||||
// cancel all previous open objects requests for listing
|
||||
cancelContexts.Range(func(key, value interface{}) bool {
|
||||
rid := key.(int64)
|
||||
if rid < messageRequest.RequestID {
|
||||
cancelFunc := value.(context.CancelFunc)
|
||||
cancelFunc()
|
||||
|
||||
cancelContexts.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const itemsPerBatch = 1000
|
||||
switch messageRequest.Mode {
|
||||
case "close":
|
||||
return
|
||||
case "cancel":
|
||||
// if we have that request id, cancel it
|
||||
if cancelFunc, ok := cancelContexts.Load(messageRequest.RequestID); ok {
|
||||
cancelFunc.(context.CancelFunc)()
|
||||
cancelContexts.Delete(messageRequest.RequestID)
|
||||
}
|
||||
case "objects":
|
||||
// start listing and writing to web socket
|
||||
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
|
||||
if err != nil {
|
||||
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
|
||||
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: ErrorWithContext(ctx, err),
|
||||
Prefix: messageRequest.Prefix,
|
||||
BucketName: messageRequest.BucketName,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var buffer []ObjectResponse
|
||||
for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
|
||||
if lsObj.Err != nil {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: ErrorWithContext(ctx, lsObj.Err),
|
||||
Prefix: messageRequest.Prefix,
|
||||
BucketName: messageRequest.BucketName,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
// if the key is same as requested prefix it would be nested directory object, so skip
|
||||
// and show only objects under the prefix
|
||||
// E.g:
|
||||
// bucket/prefix1/prefix2/ -- this should be skipped from list item.
|
||||
// bucket/prefix1/prefix2/an-object
|
||||
// bucket/prefix1/prefix2/another-object
|
||||
if messageRequest.Prefix != lsObj.Key {
|
||||
objItem := ObjectResponse{
|
||||
Name: lsObj.Key,
|
||||
Size: lsObj.Size,
|
||||
LastModified: lsObj.LastModified.Format(time.RFC3339),
|
||||
VersionID: lsObj.VersionID,
|
||||
IsLatest: lsObj.IsLatest,
|
||||
DeleteMarker: lsObj.IsDeleteMarker,
|
||||
}
|
||||
buffer = append(buffer, objItem)
|
||||
}
|
||||
|
||||
if len(buffer) >= itemsPerBatch {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
})
|
||||
buffer = nil
|
||||
}
|
||||
}
|
||||
if len(buffer) > 0 {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
})
|
||||
}
|
||||
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
RequestEnd: true,
|
||||
})
|
||||
|
||||
// if we have that request id, cancel it
|
||||
if cancelFunc, ok := cancelContexts.Load(messageRequest.RequestID); ok {
|
||||
cancelFunc.(context.CancelFunc)()
|
||||
cancelContexts.Delete(messageRequest.RequestID)
|
||||
}
|
||||
case "rewind":
|
||||
// start listing and writing to web socket
|
||||
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
|
||||
if err != nil {
|
||||
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: ErrorWithContext(ctx, err),
|
||||
Prefix: messageRequest.Prefix,
|
||||
BucketName: messageRequest.BucketName,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := wsc.conn.remoteAddress()
|
||||
|
||||
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix, clientIP)
|
||||
if err != nil {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: ErrorWithContext(ctx, err),
|
||||
Prefix: messageRequest.Prefix,
|
||||
BucketName: messageRequest.BucketName,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
mcS3C := mcClient{client: s3Client}
|
||||
|
||||
var buffer []ObjectResponse
|
||||
|
||||
for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
|
||||
if lsObj.Err != nil {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: ErrorWithContext(ctx, lsObj.Err.ToGoError()),
|
||||
Prefix: messageRequest.Prefix,
|
||||
BucketName: messageRequest.BucketName,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.Replace(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "", 1)
|
||||
|
||||
objItem := ObjectResponse{
|
||||
Name: name,
|
||||
Size: lsObj.Size,
|
||||
LastModified: lsObj.Time.Format(time.RFC3339),
|
||||
VersionID: lsObj.VersionID,
|
||||
IsLatest: lsObj.IsLatest,
|
||||
DeleteMarker: lsObj.IsDeleteMarker,
|
||||
}
|
||||
buffer = append(buffer, objItem)
|
||||
|
||||
if len(buffer) >= itemsPerBatch {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
})
|
||||
buffer = nil
|
||||
}
|
||||
|
||||
}
|
||||
if len(buffer) > 0 {
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
})
|
||||
}
|
||||
|
||||
sendWSResponse(WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
RequestEnd: true,
|
||||
})
|
||||
|
||||
// if we have that request id, cancel it
|
||||
if cancelFunc, ok := cancelContexts.Load(messageRequest.RequestID); ok {
|
||||
cancelFunc.(context.CancelFunc)()
|
||||
cancelContexts.Delete(messageRequest.RequestID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer close(done)
|
||||
|
||||
for writeM := range writeChannel {
|
||||
jsonData, err := json.Marshal(writeM)
|
||||
if err != nil {
|
||||
LogInfo("Error while marshaling the response: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = wsc.conn.writeMessage(websocket.TextMessage, jsonData)
|
||||
if err != nil {
|
||||
LogInfo("Error while writing the message: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
71
cluster/cluster.go
Normal file
71
cluster/cluster.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
direct "github.com/minio/direct-csi/pkg/clientset"
|
||||
operator "github.com/minio/operator/pkg/client/clientset/versioned"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
)
|
||||
|
||||
// getTLSClientConfig will return the right TLS configuration for the K8S client based on the configured TLS certificate
|
||||
func getTLSClientConfig() rest.TLSClientConfig {
|
||||
var defaultRootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
var customRootCAFile = getK8sAPIServerTLSRootCA()
|
||||
tlsClientConfig := rest.TLSClientConfig{}
|
||||
// if console is running inside k8s by default he will have access to the CA Cert from the k8s local authority
|
||||
if _, err := certutil.NewPool(defaultRootCAFile); err == nil {
|
||||
tlsClientConfig.CAFile = defaultRootCAFile
|
||||
}
|
||||
// if the user explicitly define a custom CA certificate, instead, we will use that
|
||||
if customRootCAFile != "" {
|
||||
if _, err := certutil.NewPool(customRootCAFile); err == nil {
|
||||
tlsClientConfig.CAFile = customRootCAFile
|
||||
}
|
||||
}
|
||||
return tlsClientConfig
|
||||
}
|
||||
|
||||
// This operation will run only once at console startup
|
||||
var tlsClientConfig = getTLSClientConfig()
|
||||
|
||||
func GetK8sConfig(token string) *rest.Config {
|
||||
config := &rest.Config{
|
||||
Host: GetK8sAPIServer(),
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
APIPath: "/",
|
||||
BearerToken: token,
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// OperatorClient returns an operator client using GetK8sConfig for its config
|
||||
func OperatorClient(token string) (*operator.Clientset, error) {
|
||||
return operator.NewForConfig(GetK8sConfig(token))
|
||||
}
|
||||
|
||||
// K8sClient returns kubernetes client using GetK8sConfig for its config
|
||||
func K8sClient(token string) (*kubernetes.Clientset, error) {
|
||||
return kubernetes.NewForConfig(GetK8sConfig(token))
|
||||
}
|
||||
|
||||
// DirectCSIClient returns Direct CSI client using GetK8sConfig for its config
|
||||
func DirectCSIClient(token string) (*direct.Clientset, error) {
|
||||
return direct.NewForConfig(GetK8sConfig(token))
|
||||
}
|
||||
121
cluster/config.go
Normal file
121
cluster/config.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
var (
|
||||
errCantDetermineMinIOImage = errors.New("can't determine MinIO Image")
|
||||
)
|
||||
|
||||
func GetK8sAPIServer() string {
|
||||
// if console is running inside a k8s pod KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT will contain the k8s api server apiServerAddress
|
||||
// if console is not running inside k8s by default will look for the k8s api server on localhost:8001 (kubectl proxy)
|
||||
// NOTE: using kubectl proxy is for local development only, since every request send to localhost:8001 will bypass service account authentication
|
||||
// more info here: https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#directly-accessing-the-rest-api
|
||||
// you can override this using CONSOLE_K8S_API_SERVER, ie use the k8s cluster from `kubectl config view`
|
||||
host, port := env.Get("KUBERNETES_SERVICE_HOST", ""), env.Get("KUBERNETES_SERVICE_PORT", "")
|
||||
apiServerAddress := "http://localhost:8001"
|
||||
if host != "" && port != "" {
|
||||
apiServerAddress = "https://" + net.JoinHostPort(host, port)
|
||||
}
|
||||
return env.Get(ConsoleK8sAPIServer, apiServerAddress)
|
||||
}
|
||||
|
||||
// If CONSOLE_K8S_API_SERVER_TLS_ROOT_CA is true console will load the certificate into the
|
||||
// http.client rootCAs pool, this is useful for testing an k8s ApiServer or when working with self-signed certificates
|
||||
func getK8sAPIServerTLSRootCA() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleK8SAPIServerTLSRootCA, ""))
|
||||
}
|
||||
|
||||
// GetNsFromFile assumes console is running inside a k8s pod and extract the current namespace from the
|
||||
// /var/run/secrets/kubernetes.io/serviceaccount/namespace file
|
||||
func GetNsFromFile() string {
|
||||
dat, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
if err != nil {
|
||||
return "default"
|
||||
}
|
||||
return string(dat)
|
||||
}
|
||||
|
||||
// Namespace will run only once at console startup
|
||||
var Namespace = GetNsFromFile()
|
||||
|
||||
// getLatestMinIOImage returns the latest docker image for MinIO if found on the internet
|
||||
func getLatestMinIOImage(client HTTPClientI) (*string, error) {
|
||||
resp, err := client.Get("https://dl.min.io/server/minio/release/linux-amd64/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var re = regexp.MustCompile(`(?m)\.\/minio\.(RELEASE.*?Z)"`)
|
||||
// look for a single match
|
||||
matches := re.FindAllStringSubmatch(string(body), 1)
|
||||
for i := range matches {
|
||||
release := matches[i][1]
|
||||
dockerImage := fmt.Sprintf("minio/minio:%s", release)
|
||||
return &dockerImage, nil
|
||||
}
|
||||
return nil, errCantDetermineMinIOImage
|
||||
}
|
||||
|
||||
var latestMinIOImage, errLatestMinIOImage = getLatestMinIOImage(
|
||||
&HTTPClient{
|
||||
Client: &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
},
|
||||
})
|
||||
|
||||
// GetMinioImage returns the image URL to be used when deploying a MinIO instance, if there is
|
||||
// a preferred image to be used (configured via ENVIRONMENT VARIABLES) GetMinioImage will return that
|
||||
// if not, GetMinioImage will try to obtain the image URL for the latest version of MinIO and return that
|
||||
func GetMinioImage() (*string, error) {
|
||||
image := strings.TrimSpace(env.Get(ConsoleMinioImage, ""))
|
||||
// if there is a preferred image configured by the user we'll always return that
|
||||
if image != "" {
|
||||
return &image, nil
|
||||
}
|
||||
if errLatestMinIOImage != nil {
|
||||
return nil, errLatestMinIOImage
|
||||
}
|
||||
return latestMinIOImage, nil
|
||||
}
|
||||
|
||||
// GetLatestMinioImage returns the latest image URL on minio repository
|
||||
func GetLatestMinioImage(client HTTPClientI) (*string, error) {
|
||||
latestMinIOImage, err := getLatestMinIOImage(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return latestMinIOImage, nil
|
||||
}
|
||||
24
cluster/const.go
Normal file
24
cluster/const.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cluster
|
||||
|
||||
const (
|
||||
ConsoleK8sAPIServer = "CONSOLE_K8S_API_SERVER"
|
||||
ConsoleK8SAPIServerTLSRootCA = "CONSOLE_K8S_API_SERVER_TLS_ROOT_CA"
|
||||
ConsoleMinioImage = "CONSOLE_MINIO_IMAGE"
|
||||
ConsoleMCImage = "CONSOLE_MC_IMAGE"
|
||||
)
|
||||
53
cluster/http_client.go
Normal file
53
cluster/http_client.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPClientI interface with all functions to be implemented
|
||||
// by mock when testing, it should include all HttpClient respective api calls
|
||||
// that are used within this project.
|
||||
type HTTPClientI interface {
|
||||
Get(url string) (resp *http.Response, err error)
|
||||
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// HTTPClient Interface implementation
|
||||
//
|
||||
// Define the structure of a http client and define the functions that are actually used
|
||||
type HTTPClient struct {
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Get implements http.Client.Get()
|
||||
func (c *HTTPClient) Get(url string) (resp *http.Response, err error) {
|
||||
return c.Client.Get(url)
|
||||
}
|
||||
|
||||
// Post implements http.Client.Post()
|
||||
func (c *HTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
return c.Client.Post(url, contentType, body)
|
||||
}
|
||||
|
||||
// Do implements http.Client.Do()
|
||||
func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/console/api"
|
||||
)
|
||||
|
||||
var appCmds = []cli.Command{
|
||||
serverCmd,
|
||||
updateCmd,
|
||||
}
|
||||
|
||||
// StartServer starts the console service
|
||||
func StartServer(ctx *cli.Context) error {
|
||||
if err := loadAllCerts(ctx); err != nil {
|
||||
// Log this as a warning and continue running console without TLS certificates
|
||||
api.LogError("Unable to load certs: %v", err)
|
||||
}
|
||||
|
||||
xctx := context.Background()
|
||||
|
||||
transport := api.PrepareSTSClientTransport(api.LocalAddress).Transport.(*http.Transport)
|
||||
if err := logger.InitializeLogger(xctx, transport); err != nil {
|
||||
fmt.Println("error InitializeLogger", err)
|
||||
logger.CriticalIf(xctx, err)
|
||||
}
|
||||
// custom error configuration
|
||||
api.LogInfo = logger.Info
|
||||
api.LogError = logger.Error
|
||||
api.LogIf = logger.LogIf
|
||||
|
||||
var rctx api.Context
|
||||
if err := rctx.Load(ctx); err != nil {
|
||||
api.LogError("argument validation failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := buildServer()
|
||||
if err != nil {
|
||||
api.LogError("Unable to initialize console server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server.Host = rctx.Host
|
||||
server.Port = rctx.HTTPPort
|
||||
// set conservative timesout for uploads
|
||||
server.ReadTimeout = 1 * time.Hour
|
||||
// no timeouts for response for downloads
|
||||
server.WriteTimeout = 0
|
||||
api.Port = strconv.Itoa(server.Port)
|
||||
api.Hostname = server.Host
|
||||
|
||||
if len(api.GlobalPublicCerts) > 0 {
|
||||
// If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect
|
||||
// plain HTTP connections to HTTPS server
|
||||
server.EnabledListeners = []string{"http", "https"}
|
||||
server.TLSPort = rctx.HTTPSPort
|
||||
// Need to store tls-port, tls-host un config variables so secure.middleware can read from there
|
||||
api.TLSPort = strconv.Itoa(server.TLSPort)
|
||||
api.Hostname = rctx.Host
|
||||
api.TLSRedirect = rctx.TLSRedirect
|
||||
}
|
||||
|
||||
defer server.Shutdown()
|
||||
|
||||
if err = server.Serve(); err != nil {
|
||||
server.Logf("error serving API: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -25,31 +25,37 @@ import (
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/console/pkg"
|
||||
"github.com/minio/pkg/v3/console"
|
||||
"github.com/minio/pkg/v3/trie"
|
||||
"github.com/minio/pkg/v3/words"
|
||||
"github.com/minio/pkg/console"
|
||||
"github.com/minio/pkg/trie"
|
||||
"github.com/minio/pkg/words"
|
||||
)
|
||||
|
||||
// Help template for Console.
|
||||
var consoleHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}
|
||||
{{.Description}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}}{{end}} [ARGS...]
|
||||
{{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}}{{end}} [ARGS...]
|
||||
|
||||
COMMANDS:
|
||||
{{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{if .VisibleFlags}}
|
||||
{{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{if .VisibleFlags}}
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
VERSION:
|
||||
{{.Version}}
|
||||
{{.Version}}
|
||||
`
|
||||
|
||||
var appCmds = []cli.Command{
|
||||
serverCmd,
|
||||
updateCmd,
|
||||
operatorCmd,
|
||||
}
|
||||
|
||||
func newApp(name string) *cli.App {
|
||||
// Collection of console commands currently supported are.
|
||||
var commands []cli.Command
|
||||
@@ -105,7 +111,7 @@ func newApp(name string) *cli.App {
|
||||
app.Commands = commands
|
||||
app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`.
|
||||
app.CustomAppHelpTemplate = consoleHelpTemplate
|
||||
app.CommandNotFound = func(_ *cli.Context, command string) {
|
||||
app.CommandNotFound = func(ctx *cli.Context, command string) {
|
||||
console.Printf("‘%s’ is not a console sub-command. See ‘console --help’.\n", command)
|
||||
closestCommands := findClosestCommands(command)
|
||||
if len(closestCommands) > 0 {
|
||||
|
||||
246
cmd/console/operator.go
Normal file
246
cmd/console/operator.go
Normal file
@@ -0,0 +1,246 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/console/operatorapi"
|
||||
"github.com/minio/console/operatorapi/operations"
|
||||
"github.com/minio/console/pkg/certs"
|
||||
)
|
||||
|
||||
// starts the server
|
||||
var operatorCmd = cli.Command{
|
||||
Name: "operator",
|
||||
Aliases: []string{"opr"},
|
||||
Usage: "Start MinIO Operator UI server",
|
||||
Action: startOperatorServer,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: restapi.GetHostname(),
|
||||
Usage: "bind to a specific HOST, HOST can be an IP or hostname",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Value: restapi.GetPort(),
|
||||
Usage: "bind to specific HTTP port",
|
||||
},
|
||||
// This is kept here for backward compatibility,
|
||||
// hostname's do not have HTTP or HTTPs
|
||||
// hostnames are opaque so using --host
|
||||
// works for both HTTP and HTTPS setup.
|
||||
cli.StringFlag{
|
||||
Name: "tls-host",
|
||||
Value: restapi.GetHostname(),
|
||||
Hidden: true,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "certs-dir",
|
||||
Value: certs.GlobalCertsCADir.Get(),
|
||||
Usage: "path to certs directory",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "tls-port",
|
||||
Value: restapi.GetTLSPort(),
|
||||
Usage: "bind to specific HTTPS port",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-redirect",
|
||||
Value: restapi.GetTLSRedirect(),
|
||||
Usage: "toggle HTTP->HTTPS redirect",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-certificate",
|
||||
Value: "",
|
||||
Usage: "path to TLS public certificate",
|
||||
Hidden: true,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-key",
|
||||
Value: "",
|
||||
Usage: "path to TLS private key",
|
||||
Hidden: true,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-ca",
|
||||
Value: "",
|
||||
Usage: "path to TLS Certificate Authority",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func buildOperatorServer() (*operatorapi.Server, error) {
|
||||
swaggerSpec, err := loads.Embedded(operatorapi.SwaggerJSON, operatorapi.FlatSwaggerJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := operations.NewOperatorAPI(swaggerSpec)
|
||||
api.Logger = operatorapi.LogInfo
|
||||
server := operatorapi.NewServer(api)
|
||||
|
||||
parser := flags.NewParser(server, flags.Default)
|
||||
parser.ShortDescription = "MinIO Console Server"
|
||||
parser.LongDescription = swaggerSpec.Spec().Info.Description
|
||||
|
||||
server.ConfigureFlags()
|
||||
|
||||
// register all APIs
|
||||
server.ConfigureAPI()
|
||||
|
||||
for _, optsGroup := range api.CommandLineOptionsGroups {
|
||||
_, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := parser.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func loadOperatorAllCerts(ctx *cli.Context) error {
|
||||
var err error
|
||||
// Set all certs and CAs directories path
|
||||
certs.GlobalCertsDir, _, err = certs.NewConfigDirFromCtx(ctx, "certs-dir", certs.DefaultCertsDir.Get)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certs.GlobalCertsCADir = &certs.ConfigDir{Path: filepath.Join(certs.GlobalCertsDir.Get(), certs.CertsCADir)}
|
||||
// check if certs and CAs directories exists or can be created
|
||||
if err = certs.MkdirAllIgnorePerm(certs.GlobalCertsCADir.Get()); err != nil {
|
||||
return fmt.Errorf("unable to create certs CA directory at %s: failed with %w", certs.GlobalCertsCADir.Get(), err)
|
||||
}
|
||||
|
||||
// load the certificates and the CAs
|
||||
operatorapi.GlobalRootCAs, operatorapi.GlobalPublicCerts, operatorapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load certificates at %s: failed with %w", certs.GlobalCertsDir.Get(), err)
|
||||
}
|
||||
|
||||
{
|
||||
// TLS flags from swagger server, used to support VMware vsphere operator version.
|
||||
swaggerServerCertificate := ctx.String("tls-certificate")
|
||||
swaggerServerCertificateKey := ctx.String("tls-key")
|
||||
swaggerServerCACertificate := ctx.String("tls-ca")
|
||||
// load tls cert and key from swagger server tls-certificate and tls-key flags
|
||||
if swaggerServerCertificate != "" && swaggerServerCertificateKey != "" {
|
||||
if err = operatorapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
x509Certs, err := certs.ParsePublicCertFile(swaggerServerCertificate)
|
||||
if err == nil {
|
||||
operatorapi.GlobalPublicCerts = append(operatorapi.GlobalPublicCerts, x509Certs...)
|
||||
}
|
||||
}
|
||||
|
||||
// load ca cert from swagger server tls-ca flag
|
||||
if swaggerServerCACertificate != "" {
|
||||
caCert, caCertErr := ioutil.ReadFile(swaggerServerCACertificate)
|
||||
if caCertErr == nil {
|
||||
operatorapi.GlobalRootCAs.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartServer starts the console service
|
||||
func startOperatorServer(ctx *cli.Context) error {
|
||||
if err := loadOperatorAllCerts(ctx); err != nil {
|
||||
// Log this as a warning and continue running console without TLS certificates
|
||||
operatorapi.LogError("Unable to load certs: %v", err)
|
||||
}
|
||||
|
||||
var rctx operatorapi.Context
|
||||
if err := rctx.Load(ctx); err != nil {
|
||||
operatorapi.LogError("argument validation failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := buildOperatorServer()
|
||||
if err != nil {
|
||||
operatorapi.LogError("Unable to initialize console server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server.Host = rctx.Host
|
||||
server.Port = rctx.HTTPPort
|
||||
// set conservative timesout for uploads
|
||||
server.ReadTimeout = 1 * time.Hour
|
||||
// no timeouts for response for downloads
|
||||
server.WriteTimeout = 0
|
||||
operatorapi.Port = strconv.Itoa(server.Port)
|
||||
operatorapi.Hostname = server.Host
|
||||
|
||||
if len(operatorapi.GlobalPublicCerts) > 0 {
|
||||
// If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect
|
||||
// plain HTTP connections to HTTPS server
|
||||
server.EnabledListeners = []string{"http", "https"}
|
||||
server.TLSPort = rctx.HTTPSPort
|
||||
// Need to store tls-port, tls-host un config variables so secure.middleware can read from there
|
||||
operatorapi.TLSPort = strconv.Itoa(server.TLSPort)
|
||||
operatorapi.Hostname = rctx.Host
|
||||
operatorapi.TLSRedirect = rctx.TLSRedirect
|
||||
}
|
||||
|
||||
defer server.Shutdown()
|
||||
|
||||
// subnet license refresh process
|
||||
go func() {
|
||||
// start refreshing subnet license after 5 seconds..
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
failedAttempts := 0
|
||||
for {
|
||||
if err := operatorapi.RefreshLicense(); err != nil {
|
||||
operatorapi.LogError("Refreshing subnet license failed: %v", err)
|
||||
failedAttempts++
|
||||
// end license refresh after 3 consecutive failed attempts
|
||||
if failedAttempts >= 3 {
|
||||
return
|
||||
}
|
||||
// wait 5 minutes and retry again
|
||||
time.Sleep(time.Minute * 5)
|
||||
continue
|
||||
}
|
||||
// if license refreshed successfully reset the counter
|
||||
failedAttempts = 0
|
||||
// try to refresh license every 24 hrs
|
||||
time.Sleep(time.Hour * 24)
|
||||
}
|
||||
}()
|
||||
|
||||
return server.Serve()
|
||||
}
|
||||
@@ -18,16 +18,18 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/console/api"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/pkg/certs"
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/minio/console/restapi/operations"
|
||||
)
|
||||
|
||||
// starts the server
|
||||
@@ -39,12 +41,12 @@ var serverCmd = cli.Command{
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: api.GetHostname(),
|
||||
Value: restapi.GetHostname(),
|
||||
Usage: "bind to a specific HOST, HOST can be an IP or hostname",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Value: api.GetPort(),
|
||||
Value: restapi.GetPort(),
|
||||
Usage: "bind to specific HTTP port",
|
||||
},
|
||||
// This is kept here for backward compatibility,
|
||||
@@ -53,7 +55,7 @@ var serverCmd = cli.Command{
|
||||
// works for both HTTP and HTTPS setup.
|
||||
cli.StringFlag{
|
||||
Name: "tls-host",
|
||||
Value: api.GetHostname(),
|
||||
Value: restapi.GetHostname(),
|
||||
Hidden: true,
|
||||
},
|
||||
cli.StringFlag{
|
||||
@@ -63,12 +65,12 @@ var serverCmd = cli.Command{
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "tls-port",
|
||||
Value: api.GetTLSPort(),
|
||||
Value: restapi.GetTLSPort(),
|
||||
Usage: "bind to specific HTTPS port",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-redirect",
|
||||
Value: api.GetTLSRedirect(),
|
||||
Value: restapi.GetTLSRedirect(),
|
||||
Usage: "toggle HTTP->HTTPS redirect",
|
||||
},
|
||||
cli.StringFlag{
|
||||
@@ -92,15 +94,15 @@ var serverCmd = cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func buildServer() (*api.Server, error) {
|
||||
swaggerSpec, err := loads.Embedded(api.SwaggerJSON, api.FlatSwaggerJSON)
|
||||
func buildServer() (*restapi.Server, error) {
|
||||
swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
consoleAPI := operations.NewConsoleAPI(swaggerSpec)
|
||||
consoleAPI.Logger = api.LogInfo
|
||||
server := api.NewServer(consoleAPI)
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api.Logger = restapi.LogInfo
|
||||
server := restapi.NewServer(api)
|
||||
|
||||
parser := flags.NewParser(server, flags.Default)
|
||||
parser.ShortDescription = "MinIO Console Server"
|
||||
@@ -111,7 +113,7 @@ func buildServer() (*api.Server, error) {
|
||||
// register all APIs
|
||||
server.ConfigureAPI()
|
||||
|
||||
for _, optsGroup := range consoleAPI.CommandLineOptionsGroups {
|
||||
for _, optsGroup := range api.CommandLineOptionsGroups {
|
||||
_, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -140,7 +142,7 @@ func loadAllCerts(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
// load the certificates and the CAs
|
||||
api.GlobalRootCAs, api.GlobalPublicCerts, api.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
|
||||
restapi.GlobalRootCAs, restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load certificates at %s: failed with %w", certs.GlobalCertsDir.Get(), err)
|
||||
}
|
||||
@@ -152,27 +154,71 @@ func loadAllCerts(ctx *cli.Context) error {
|
||||
swaggerServerCACertificate := ctx.String("tls-ca")
|
||||
// load tls cert and key from swagger server tls-certificate and tls-key flags
|
||||
if swaggerServerCertificate != "" && swaggerServerCertificateKey != "" {
|
||||
if err = api.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
|
||||
if err = restapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
x509Certs, err := certs.ParsePublicCertFile(swaggerServerCertificate)
|
||||
if err == nil {
|
||||
api.GlobalPublicCerts = append(api.GlobalPublicCerts, x509Certs...)
|
||||
restapi.GlobalPublicCerts = append(restapi.GlobalPublicCerts, x509Certs...)
|
||||
}
|
||||
}
|
||||
|
||||
// load ca cert from swagger server tls-ca flag
|
||||
if swaggerServerCACertificate != "" {
|
||||
caCert, caCertErr := os.ReadFile(swaggerServerCACertificate)
|
||||
caCert, caCertErr := ioutil.ReadFile(swaggerServerCACertificate)
|
||||
if caCertErr == nil {
|
||||
api.GlobalRootCAs.AppendCertsFromPEM(caCert)
|
||||
restapi.GlobalRootCAs.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if api.GlobalTLSCertsManager != nil {
|
||||
api.GlobalTLSCertsManager.ReloadOnSignal(syscall.SIGHUP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartServer starts the console service
|
||||
func StartServer(ctx *cli.Context) error {
|
||||
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
|
||||
return startOperatorServer(ctx)
|
||||
}
|
||||
|
||||
if err := loadAllCerts(ctx); err != nil {
|
||||
// Log this as a warning and continue running console without TLS certificates
|
||||
restapi.LogError("Unable to load certs: %v", err)
|
||||
}
|
||||
|
||||
var rctx restapi.Context
|
||||
if err := rctx.Load(ctx); err != nil {
|
||||
restapi.LogError("argument validation failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := buildServer()
|
||||
if err != nil {
|
||||
restapi.LogError("Unable to initialize console server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server.Host = rctx.Host
|
||||
server.Port = rctx.HTTPPort
|
||||
// set conservative timesout for uploads
|
||||
server.ReadTimeout = 1 * time.Hour
|
||||
// no timeouts for response for downloads
|
||||
server.WriteTimeout = 0
|
||||
restapi.Port = strconv.Itoa(server.Port)
|
||||
restapi.Hostname = server.Host
|
||||
|
||||
if len(restapi.GlobalPublicCerts) > 0 {
|
||||
// If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect
|
||||
// plain HTTP connections to HTTPS server
|
||||
server.EnabledListeners = []string{"http", "https"}
|
||||
server.TLSPort = rctx.HTTPSPort
|
||||
// Need to store tls-port, tls-host un config variables so secure.middleware can read from there
|
||||
restapi.TLSPort = strconv.Itoa(server.TLSPort)
|
||||
restapi.Hostname = rctx.Host
|
||||
restapi.TLSRedirect = rctx.TLSRedirect
|
||||
}
|
||||
|
||||
defer server.Shutdown()
|
||||
|
||||
return server.Serve()
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ var updateCmd = cli.Command{
|
||||
Action: updateInplace,
|
||||
}
|
||||
|
||||
func updateInplace(_ *cli.Context) error {
|
||||
func updateInplace(ctx *cli.Context) error {
|
||||
transport := getUpdateTransport(30 * time.Second)
|
||||
rel, err := getLatestRelease(transport)
|
||||
if err != nil {
|
||||
|
||||
39
docs/console_operator_mode.md
Normal file
39
docs/console_operator_mode.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Running Console in Operator mode
|
||||
|
||||
`Console` will authenticate against `Kubernetes`using bearer tokens via HTTP `Authorization` header. The user will provide this token once
|
||||
in the login form, Console will validate it against Kubernetes (list apis) and if valid will generate and return a new Console sessions
|
||||
with encrypted claims (the user Service account token will be inside the session encrypted token
|
||||
|
||||
# Kubernetes
|
||||
|
||||
The provided `JWT token` corresponds to the `Kubernetes service account` that `Console` will use to run tasks on behalf of the
|
||||
user, ie: list, create, edit, delete tenants, storage class, etc.
|
||||
|
||||
|
||||
# Development
|
||||
|
||||
If console is running inside a k8s pod `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` will contain the k8s api server apiServerAddress
|
||||
if console is not running inside k8s by default will look for the k8s api server on `localhost:8001` (kubectl proxy)
|
||||
|
||||
If you are running console in your local environment and wish to make request to `Kubernetes` you can set `CONSOLE_K8S_API_SERVER`, if
|
||||
the environment variable is not present by default `Console` will use `"http://localhost:8001"`, additionally you will need to set the
|
||||
`CONSOLE_OPERATOR_MODE=on` variable to make Console display the Operator UI.
|
||||
|
||||
NOTE: using `kubectl` proxy is for local development only, since every request send to localhost:8001 will bypass service account authentication
|
||||
more info here: https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#directly-accessing-the-rest-api
|
||||
you can override this using `CONSOLE_K8S_API_SERVER`, ie use the k8s cluster from `kubectl config view`
|
||||
|
||||
## Extract the Service account token and use it with Console
|
||||
|
||||
For local development you can use the jwt associated to the `console-sa` service account, you can get the token running
|
||||
the following command in your terminal:
|
||||
|
||||
```
|
||||
kubectl get secret $(kubectl get serviceaccount console-sa -o jsonpath="{.secrets[0].name}") -o jsonpath="{.data.token}" | base64 --decode
|
||||
```
|
||||
|
||||
Then run the Console server
|
||||
|
||||
```
|
||||
CONSOLE_OPERATOR_MODE=on ./console server
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
hooks: {
|
||||
onInsertPathParam: (paramName) => `encodeURIComponent(${paramName})`,
|
||||
},
|
||||
};
|
||||
178
go.mod
178
go.mod
@@ -1,152 +1,44 @@
|
||||
module github.com/minio/console
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.8
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/cheggaaa/pb/v3 v3.1.7
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-openapi/errors v0.22.0
|
||||
github.com/go-openapi/loads v0.22.0
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
github.com/go-openapi/spec v0.21.0
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/go-openapi/swag v0.23.1
|
||||
github.com/go-openapi/validate v0.24.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/minio/cli v1.24.2
|
||||
github.com/minio/highwayhash v1.0.3
|
||||
github.com/minio/kes v0.24.0
|
||||
github.com/minio/madmin-go/v3 v3.0.98
|
||||
github.com/minio/mc v0.0.0-20250313080218-cf909e1063a9
|
||||
github.com/minio/minio-go/v7 v7.0.88
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/minio/websocket v1.6.0
|
||||
github.com/cheggaaa/pb/v3 v3.0.6
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-openapi/errors v0.19.9
|
||||
github.com/go-openapi/loads v0.20.2
|
||||
github.com/go-openapi/runtime v0.19.24
|
||||
github.com/go-openapi/spec v0.20.3
|
||||
github.com/go-openapi/strfmt v0.20.0
|
||||
github.com/go-openapi/swag v0.19.14
|
||||
github.com/go-openapi/validate v0.20.2
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf
|
||||
github.com/minio/kes v0.11.0
|
||||
github.com/minio/madmin-go v1.0.17
|
||||
github.com/minio/mc v0.0.0-20210626002108-cebf3318546f
|
||||
github.com/minio/minio-go/v7 v7.0.13-0.20210715203016-9e713532886e
|
||||
github.com/minio/operator v0.0.0-20210812082324-26350f153661
|
||||
github.com/minio/operator/logsearchapi v0.0.0-20210812082324-26350f153661
|
||||
github.com/minio/pkg v1.0.8
|
||||
github.com/minio/selfupdate v0.3.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/rs/xid v1.2.1
|
||||
github.com/secure-io/sio-go v0.3.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/unrolled/secure v1.17.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
// Added to include security fix for
|
||||
// https://github.com/golang/go/issues/56152
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/unrolled/secure v1.0.7
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.21.1
|
||||
k8s.io/apimachinery v0.21.1
|
||||
k8s.io/client-go v0.21.1
|
||||
)
|
||||
|
||||
require github.com/minio/pkg/v3 v3.1.2
|
||||
|
||||
require (
|
||||
aead.dev/mem v0.2.0 // indirect
|
||||
aead.dev/minisign v0.3.0 // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.4 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.0.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cheggaaa/pb v1.0.29 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.4 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/minio/colorjson v1.0.8 // indirect
|
||||
github.com/minio/crc64nvme v1.0.1 // indirect
|
||||
github.com/minio/filepath v1.0.0 // indirect
|
||||
github.com/minio/kms-go/kes v0.3.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/pkg/xattr v0.4.10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/posener/complete v1.2.3 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/prom2json v1.4.1 // indirect
|
||||
github.com/prometheus/prometheus v0.302.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rjeczalik/notify v0.9.3 // indirect
|
||||
github.com/safchain/ethtool v0.5.10 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.2.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/vbauerster/mpb/v8 v8.9.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.19 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.19 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.19 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.29.1
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
@@ -6,7 +20,10 @@ set -o pipefail
|
||||
|
||||
SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/..
|
||||
|
||||
GO111MODULE=off go get -d k8s.io/code-generator/...
|
||||
go get -d k8s.io/code-generator/...
|
||||
|
||||
# Checkout code-generator to compatible version
|
||||
#(cd $GOPATH/src/k8s.io/code-generator && git checkout origin/release-1.14 -B release-1.14)
|
||||
|
||||
REPOSITORY=github.com/minio/console
|
||||
$GOPATH/src/k8s.io/code-generator/generate-groups.sh all \
|
||||
|
||||
BIN
images/pic1.png
BIN
images/pic1.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.8 MiB |
BIN
images/pic2.png
BIN
images/pic2.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.5 MiB |
BIN
images/pic3.png
BIN
images/pic3.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -1,173 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/minio/console/api"
|
||||
"github.com/minio/console/api/operations"
|
||||
)
|
||||
|
||||
var token string
|
||||
|
||||
func inspectHTTPResponse(httpResponse *http.Response) string {
|
||||
/*
|
||||
Helper function to inspect the content of a HTTP response.
|
||||
*/
|
||||
b, err := io.ReadAll(httpResponse.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
return "Http Response: " + string(b)
|
||||
}
|
||||
|
||||
func initConsoleServer() (*api.Server, error) {
|
||||
// os.Setenv("CONSOLE_MINIO_SERVER", "localhost:9000")
|
||||
|
||||
swaggerSpec, err := loads.Embedded(api.SwaggerJSON, api.FlatSwaggerJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
noLog := func(string, ...interface{}) {
|
||||
// nothing to log
|
||||
}
|
||||
|
||||
// Initialize MinIO loggers
|
||||
api.LogInfo = noLog
|
||||
api.LogError = noLog
|
||||
|
||||
consoleAPI := operations.NewConsoleAPI(swaggerSpec)
|
||||
consoleAPI.Logger = noLog
|
||||
|
||||
server := api.NewServer(consoleAPI)
|
||||
// register all APIs
|
||||
server.ConfigureAPI()
|
||||
|
||||
// api.GlobalRootCAs, api.GlobalPublicCerts, api.GlobalTLSCertsManager = globalRootCAs, globalPublicCerts, globalTLSCerts
|
||||
|
||||
consolePort, _ := strconv.Atoi("9090")
|
||||
|
||||
server.Host = "0.0.0.0"
|
||||
server.Port = consolePort
|
||||
api.Port = "9090"
|
||||
api.Hostname = "0.0.0.0"
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// start console server
|
||||
go func() {
|
||||
fmt.Println("start server")
|
||||
srv, err := initConsoleServer()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Println("init fail")
|
||||
return
|
||||
}
|
||||
srv.Serve()
|
||||
}()
|
||||
|
||||
fmt.Println("sleeping")
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
// get login credentials
|
||||
|
||||
requestData := map[string]string{
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadmin",
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestData)
|
||||
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost:9090/api/v1/login", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if response != nil {
|
||||
for _, cookie := range response.Cookies() {
|
||||
if cookie.Name == "token" {
|
||||
token = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
log.Println("authentication token not found in cookies response")
|
||||
return
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
requestDataAdd := map[string]interface{}{
|
||||
"name": "test1",
|
||||
}
|
||||
|
||||
requestDataJSON, _ = json.Marshal(requestDataAdd)
|
||||
|
||||
requestDataBody = bytes.NewReader(requestDataJSON)
|
||||
|
||||
// delete bucket
|
||||
request, err = http.NewRequest("DELETE", "http://localhost:9090/api/v1/buckets/test1", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err = client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if response != nil {
|
||||
fmt.Println("DELETE StatusCode:", response.StatusCode)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoginStrategy(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// image for now:
|
||||
// minio: 9000
|
||||
// console: 9090
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
// copy query params
|
||||
request, err := http.NewRequest("GET", "http://localhost:9090/api/v1/login", nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
assert.Nil(err)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if response != nil {
|
||||
bodyBytes, _ := io.ReadAll(response.Body)
|
||||
|
||||
loginDetails := models.LoginDetails{}
|
||||
|
||||
err = json.Unmarshal(bodyBytes, &loginDetails)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal(models.LoginDetailsLoginStrategyForm, loginDetails.LoginStrategy, "Login Details don't match")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// image for now:
|
||||
// minio: 9000
|
||||
// console: 9090
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
requestData := map[string]string{
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadmin",
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestData)
|
||||
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost:9090/api/v1/login", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
|
||||
assert.NotNil(response, "Login response is nil")
|
||||
assert.Nil(err, "Login errored out")
|
||||
|
||||
var loginToken string
|
||||
|
||||
for _, cookie := range response.Cookies() {
|
||||
if cookie.Name == "token" {
|
||||
loginToken = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if loginToken == "" {
|
||||
log.Println("authentication token not found in cookies response")
|
||||
return
|
||||
}
|
||||
logoutRequest := bytes.NewReader([]byte("{}"))
|
||||
request, err = http.NewRequest("POST", "http://localhost:9090/api/v1/logout", logoutRequest)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", loginToken))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err = client.Do(request)
|
||||
assert.NotNil(response, "Logout response is nil")
|
||||
assert.Nil(err, "Logout errored out")
|
||||
assert.Equal(response.StatusCode, 200)
|
||||
}
|
||||
|
||||
func TestLoginExtraSpaces(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
requestData := map[string]string{
|
||||
"accessKey": " minioadmin ",
|
||||
"secretKey": "minioadmin",
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestData)
|
||||
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost:9090/api/v1/login", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
|
||||
assert.Equal(204, response.StatusCode, "Login request should succeed")
|
||||
assert.NotNil(response, "Login response is nil")
|
||||
assert.Nil(err, "Login errored out")
|
||||
}
|
||||
|
||||
func TestBadLogin(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
requestData := map[string]string{
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadminbad",
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestData)
|
||||
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost:9090/api/v1/login", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
|
||||
assert.Equal(401, response.StatusCode, "Login request not rejected")
|
||||
assert.NotNil(response, "Login response is nil")
|
||||
assert.Nil(err, "Login errored out")
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestObjectGet(t *testing.T) {
|
||||
// for setup we'll create a bucket and upload a file
|
||||
endpoint := "localhost:9000"
|
||||
accessKeyID := "minioadmin"
|
||||
secretAccessKey := "minioadmin"
|
||||
|
||||
// Initialize minio client object.
|
||||
minioClient, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
||||
Secure: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
bucketName := fmt.Sprintf("testbucket-%d", rand.Intn(1000-1)+1)
|
||||
err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1", ObjectLocking: true})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// upload a simple file
|
||||
fakeFile := "12345678"
|
||||
fileReader := strings.NewReader(fakeFile)
|
||||
|
||||
_, err = minioClient.PutObject(
|
||||
context.Background(),
|
||||
bucketName,
|
||||
"myobject", fileReader, int64(len(fakeFile)), minio.PutObjectOptions{ContentType: "application/octet-stream"})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
_, err = minioClient.PutObject(
|
||||
context.Background(),
|
||||
bucketName,
|
||||
"myobject.jpg", fileReader, int64(len(fakeFile)), minio.PutObjectOptions{ContentType: "application/octet-stream"})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert := assert.New(t)
|
||||
type args struct {
|
||||
encodedPrefix string
|
||||
versionID string
|
||||
bytesRange string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Preview Object",
|
||||
args: args{
|
||||
encodedPrefix: "myobject",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Preview image",
|
||||
args: args{
|
||||
encodedPrefix: "myobject.jpg",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get Range of bytes",
|
||||
args: args{
|
||||
encodedPrefix: "myobject.jpg",
|
||||
bytesRange: "bytes=1-4",
|
||||
},
|
||||
expectedStatus: 206,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get Range of bytes empty start",
|
||||
args: args{
|
||||
encodedPrefix: "myobject.jpg",
|
||||
bytesRange: "bytes=-4",
|
||||
},
|
||||
expectedStatus: 206,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get Invalid Range of bytes",
|
||||
args: args{
|
||||
encodedPrefix: "myobject.jpg",
|
||||
bytesRange: "bytes=9-12",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get Larger Range of bytes empty start",
|
||||
args: args{
|
||||
encodedPrefix: "myobject.jpg",
|
||||
bytesRange: "bytes=-12",
|
||||
},
|
||||
expectedStatus: 206,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get invalid seek start Range of bytes",
|
||||
args: args{
|
||||
encodedPrefix: "myobject.jpg",
|
||||
bytesRange: "bytes=12-16",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
destination := fmt.Sprintf("/api/v1/buckets/%s/objects/download?preview=true&prefix=%s&version_id=%s", url.PathEscape(bucketName), url.QueryEscape(tt.args.encodedPrefix), url.QueryEscape(tt.args.versionID))
|
||||
finalURL := fmt.Sprintf("http://localhost:9090%s", destination)
|
||||
request, err := http.NewRequest("GET", finalURL, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
if tt.args.bytesRange != "" {
|
||||
request.Header.Add("Range", tt.args.bytesRange)
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
fmt.Printf("Console server Response: %v\nErr: %v\n", response, err)
|
||||
|
||||
assert.NotNil(response, fmt.Sprintf("%s response object is nil", tt.name))
|
||||
assert.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, fmt.Sprintf("%s returned the wrong status code", tt.name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func downloadMultipleFiles(bucketName string, objects []string) (*http.Response, error) {
|
||||
requestURL := fmt.Sprintf("http://localhost:9090/api/v1/buckets/%s/objects/download-multiple", url.PathEscape(bucketName))
|
||||
|
||||
postReqParams, _ := json.Marshal(objects)
|
||||
reqBody := bytes.NewReader(postReqParams)
|
||||
|
||||
request, err := http.NewRequest(
|
||||
"POST", requestURL, reqBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func TestDownloadMultipleFiles(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
type args struct {
|
||||
bucketName string
|
||||
objectLis []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Test empty Bucket",
|
||||
args: args{
|
||||
bucketName: "",
|
||||
},
|
||||
expectedStatus: 400,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "Test empty object list",
|
||||
args: args{
|
||||
bucketName: "test-bucket",
|
||||
},
|
||||
expectedStatus: 400,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "Test with bucket and object list",
|
||||
args: args{
|
||||
bucketName: "test-bucket",
|
||||
objectLis: []string{
|
||||
"my-object.txt",
|
||||
"test-prefix/",
|
||||
"test-prefix/nested-prefix/",
|
||||
"test-prefix/nested-prefix/deep-nested/",
|
||||
},
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
resp, err := downloadMultipleFiles(tt.args.bucketName, tt.args.objectLis)
|
||||
if tt.expectedError {
|
||||
assert.Nil(err)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if resp != nil {
|
||||
assert.NotNil(resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartProfiling(t *testing.T) {
|
||||
testAssert := assert.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "start/stop profiling",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
files := map[string]bool{
|
||||
"profile-127.0.0.1:9000-goroutines.txt": false,
|
||||
"profile-127.0.0.1:9000-goroutines-before.txt": false,
|
||||
"profile-127.0.0.1:9000-goroutines-before,debug=2.txt": false,
|
||||
"profile-127.0.0.1:9000-threads-before.pprof": false,
|
||||
"profile-127.0.0.1:9000-mem.pprof": false,
|
||||
"profile-127.0.0.1:9000-threads.pprof": false,
|
||||
"profile-127.0.0.1:9000-cpu.pprof": false,
|
||||
"profile-127.0.0.1:9000-mem-before.pprof": false,
|
||||
"profile-127.0.0.1:9000-block.pprof": false,
|
||||
"profile-127.0.0.1:9000-trace.trace": false,
|
||||
"profile-127.0.0.1:9000-mutex.pprof": false,
|
||||
"profile-127.0.0.1:9000-mutex-before.pprof": false,
|
||||
}
|
||||
|
||||
wsDestination := "/ws/profile?types=cpu,mem,block,mutex,trace,threads,goroutines"
|
||||
wsFinalURL := fmt.Sprintf("ws://localhost:9090%s", wsDestination)
|
||||
|
||||
ws, _, err := websocket.DefaultDialer.Dial(wsFinalURL, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
_, zipFileBytes, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
filetype := http.DetectContentType(zipFileBytes)
|
||||
testAssert.Equal("application/zip", filetype)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(zipFileBytes), int64(len(zipFileBytes)))
|
||||
if err != nil {
|
||||
testAssert.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
|
||||
}
|
||||
|
||||
// Read all the files from zip archive
|
||||
for _, zipFile := range zipReader.File {
|
||||
files[zipFile.Name] = true
|
||||
}
|
||||
|
||||
for k, v := range files {
|
||||
testAssert.Equal(true, v, fmt.Sprintf("%s : compressed file expected to have %v file inside", tt.name, k))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
subnet license= api_key= proxy=
|
||||
# callhome enable=off frequency=24h
|
||||
# site name= region=
|
||||
# api requests_max=0 requests_deadline=10s cluster_deadline=10s cors_allow_origin=* remote_transport_deadline=2h list_quorum=strict replication_priority=auto transition_workers=100 stale_uploads_cleanup_interval=6h stale_uploads_expiry=24h delete_cleanup_interval=5m disable_odirect=off gzip_objects=off
|
||||
# scanner speed=default
|
||||
# compression enable=off allow_encryption=off extensions=.txt,.log,.csv,.json,.tar,.xml,.bin mime_types=text/*,application/json,application/xml,binary/octet-stream
|
||||
# identity_openid enable= display_name= config_url= client_id= client_secret= claim_name=policy claim_userinfo= role_policy= claim_prefix= redirect_uri= redirect_uri_dynamic=off scopes= vendor= keycloak_realm= keycloak_admin_url=
|
||||
# identity_ldap server_addr= srv_record_name= user_dn_search_base_dn= user_dn_search_filter= group_search_filter= group_search_base_dn= tls_skip_verify=off server_insecure=off server_starttls=off lookup_bind_dn= lookup_bind_password=
|
||||
# identity_tls skip_verify=off
|
||||
# identity_plugin url= auth_token= role_policy= role_id=
|
||||
# policy_plugin url= auth_token= enable_http2=off
|
||||
# logger_webhook enable=off endpoint= auth_token= client_cert= client_key= queue_size=100000
|
||||
# audit_webhook enable=off endpoint= auth_token= client_cert= client_key= queue_size=100000
|
||||
# audit_kafka enable=off topic= brokers= sasl_username= sasl_password= sasl_mechanism=plain client_tls_cert= client_tls_key= tls_client_auth=0 sasl=off tls=off tls_skip_verify=off version=
|
||||
# notify_webhook enable=off endpoint= auth_token= queue_limit=0 queue_dir= client_cert= client_key=
|
||||
# notify_amqp enable=off url= exchange= exchange_type= routing_key= mandatory=off durable=off no_wait=off internal=off auto_deleted=off delivery_mode=0 publisher_confirms=off queue_limit=0 queue_dir=
|
||||
# notify_kafka enable=off topic= brokers= sasl_username= sasl_password= sasl_mechanism=plain client_tls_cert= client_tls_key= tls_client_auth=0 sasl=off tls=off tls_skip_verify=off queue_limit=0 queue_dir= version=
|
||||
# notify_mqtt enable=off broker= topic= password= username= qos=0 keep_alive_interval=0s reconnect_interval=0s queue_dir= queue_limit=0
|
||||
# notify_nats enable=off address= subject= username= password= token= tls=off tls_skip_verify=off cert_authority= client_cert= client_key= ping_interval=0 jetstream=off streaming=off streaming_async=off streaming_max_pub_acks_in_flight=0 streaming_cluster_id= queue_dir= queue_limit=0
|
||||
# notify_nsq enable=off nsqd_address= topic= tls=off tls_skip_verify=off queue_dir= queue_limit=0
|
||||
# notify_mysql enable=off format=namespace dsn_string= table= queue_dir= queue_limit=0 max_open_connections=2
|
||||
# notify_postgres enable=off format=namespace connection_string= table= queue_dir= queue_limit=0 max_open_connections=2
|
||||
# notify_elasticsearch enable=off url= format=namespace index= queue_dir= queue_limit=0 username= password=
|
||||
# notify_redis enable=off format=namespace address= key= password= queue_dir= queue_limit=0
|
||||
# etcd endpoints= path_prefix= coredns_path=/skydns client_cert= client_cert_key=
|
||||
# cache drives= exclude= expiry=90 quota=80 after=0 watermark_low=70 watermark_high=80 range=on commit=
|
||||
# storage_class standard= rrs=EC:1
|
||||
# heal bitrotscan=off max_sleep=1s max_io=100
|
||||
File diff suppressed because it is too large
Load Diff
44
k8s/create-kind.sh
Executable file
44
k8s/create-kind.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# setup environment variables based on flags to see if we should build the docker containers again
|
||||
CONSOLE_DOCKER="true"
|
||||
|
||||
# evaluate flags
|
||||
# `-m` for console
|
||||
|
||||
|
||||
while getopts ":m:" opt; do
|
||||
case $opt in
|
||||
m)
|
||||
CONSOLE_DOCKER="$OPTARG"
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
echo "Option -$OPTARG requires an argument." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Provisioning Kind"
|
||||
kind create cluster --config kind-cluster.yaml
|
||||
echo "Remove Master Taint"
|
||||
kubectl taint nodes --all node-role.kubernetes.io/master-
|
||||
echo "Install Contour"
|
||||
kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
|
||||
kubectl patch daemonsets -n projectcontour envoy -p '{"spec":{"template":{"spec":{"nodeSelector":{"ingress-ready":"true"},"tolerations":[{"key":"node-role.kubernetes.io/master","operator":"Equal","effect":"NoSchedule"}]}}}}'
|
||||
echo "install metrics server"
|
||||
kubectl apply -f metrics-dev.yaml
|
||||
|
||||
# Whether or not to build the m3 container and load it to kind or just load it
|
||||
if [[ $CONSOLE_DOCKER == "true" ]]; then
|
||||
# Build mkube
|
||||
make --directory=".." k8sdev TAG=minio/console:latest
|
||||
else
|
||||
kind load docker-image minio/console:latest
|
||||
fi
|
||||
|
||||
echo "done"
|
||||
22
k8s/kind-cluster.yaml
Normal file
22
k8s/kind-cluster.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# three node (two workers) cluster config
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: InitConfiguration
|
||||
nodeRegistration:
|
||||
kubeletExtraArgs:
|
||||
node-labels: "ingress-ready=true"
|
||||
extraPortMappings:
|
||||
- containerPort: 80
|
||||
hostPort: 8844
|
||||
protocol: TCP
|
||||
- containerPort: 443
|
||||
hostPort: 8843
|
||||
protocol: TCP
|
||||
#- role: worker
|
||||
#- role: worker
|
||||
#- role: worker
|
||||
#- role: worker
|
||||
153
k8s/metrics-dev.yaml
Normal file
153
k8s/metrics-dev.yaml
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system:aggregated-metrics-reader
|
||||
labels:
|
||||
rbac.authorization.k8s.io/aggregate-to-view: "true"
|
||||
rbac.authorization.k8s.io/aggregate-to-edit: "true"
|
||||
rbac.authorization.k8s.io/aggregate-to-admin: "true"
|
||||
rules:
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources: ["pods", "nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: metrics-server:system:auth-delegator
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system:auth-delegator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: metrics-server-auth-reader
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: extension-apiserver-authentication-reader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system:metrics-server
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
- nodes
|
||||
- nodes/stats
|
||||
- namespaces
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: system:metrics-server
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system:metrics-server
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1beta1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: v1beta1.metrics.k8s.io
|
||||
spec:
|
||||
service:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
group: metrics.k8s.io
|
||||
version: v1beta1
|
||||
insecureSkipTLSVerify: true
|
||||
groupPriorityMinimum: 100
|
||||
versionPriority: 100
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
labels:
|
||||
kubernetes.io/name: "Metrics-server"
|
||||
kubernetes.io/cluster-service: "true"
|
||||
spec:
|
||||
selector:
|
||||
k8s-app: metrics-server
|
||||
ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
targetPort: main-port
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: metrics-server
|
||||
namespace: kube-system
|
||||
labels:
|
||||
k8s-app: metrics-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: metrics-server
|
||||
template:
|
||||
metadata:
|
||||
name: metrics-server
|
||||
labels:
|
||||
k8s-app: metrics-server
|
||||
spec:
|
||||
serviceAccountName: metrics-server
|
||||
volumes:
|
||||
# mount in tmp so we can safely use from-scratch images and/or read-only containers
|
||||
- name: tmp-dir
|
||||
emptyDir: {}
|
||||
containers:
|
||||
- name: metrics-server
|
||||
image: k8s.gcr.io/metrics-server-amd64:v0.3.6
|
||||
args:
|
||||
- --cert-dir=/tmp
|
||||
- --secure-port=4443
|
||||
- --kubelet-insecure-tls
|
||||
- --kubelet-preferred-address-types=InternalIP
|
||||
ports:
|
||||
- name: main-port
|
||||
containerPort: 4443
|
||||
protocol: TCP
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
imagePullPolicy: Always
|
||||
volumeMounts:
|
||||
- name: tmp-dir
|
||||
mountPath: /tmp
|
||||
nodeSelector:
|
||||
beta.kubernetes.io/os: linux
|
||||
kubernetes.io/arch: "amd64"
|
||||
12
k8s/operator-console/base/console-cluster-role-binding.yaml
Normal file
12
k8s/operator-console/base/console-cluster-role-binding.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: console-sa-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: console-sa-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: console-sa
|
||||
namespace: default
|
||||
234
k8s/operator-console/base/console-cluster-role.yaml
Normal file
234
k8s/operator-console/base/console-cluster-role.yaml
Normal file
@@ -0,0 +1,234 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: console-sa-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- create
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- deletecollection
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
- services
|
||||
- events
|
||||
- resourcequotas
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- create
|
||||
- list
|
||||
- patch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- create
|
||||
- list
|
||||
- patch
|
||||
- delete
|
||||
- deletecollection
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumeclaims
|
||||
verbs:
|
||||
- deletecollection
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
- update
|
||||
- apiGroups:
|
||||
- "storage.k8s.io"
|
||||
resources:
|
||||
- storageclasses
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- create
|
||||
- list
|
||||
- patch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets
|
||||
- deployments
|
||||
verbs:
|
||||
- get
|
||||
- create
|
||||
- list
|
||||
- patch
|
||||
- watch
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- get
|
||||
- create
|
||||
- list
|
||||
- patch
|
||||
- watch
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- "certificates.k8s.io"
|
||||
resources:
|
||||
- "certificatesigningrequests"
|
||||
- "certificatesigningrequests/approval"
|
||||
- "certificatesigningrequests/status"
|
||||
verbs:
|
||||
- update
|
||||
- create
|
||||
- get
|
||||
- apiGroups:
|
||||
- minio.min.io
|
||||
resources:
|
||||
- "*"
|
||||
verbs:
|
||||
- "*"
|
||||
- apiGroups:
|
||||
- min.io
|
||||
resources:
|
||||
- "*"
|
||||
verbs:
|
||||
- "*"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumeclaims
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- patch
|
||||
- apiGroups:
|
||||
- snapshot.storage.k8s.io
|
||||
resources:
|
||||
- volumesnapshots
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- snapshot.storage.k8s.io
|
||||
resources:
|
||||
- volumesnapshotcontents
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- storage.k8s.io
|
||||
resources:
|
||||
- csinodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- storage.k8s.io
|
||||
resources:
|
||||
- volumeattachments
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- endpoints
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- direct.csi.min.io
|
||||
resources:
|
||||
- volumes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- apiextensions.k8s.io
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- direct.csi.min.io
|
||||
resources:
|
||||
- directcsidrives
|
||||
- directcsivolumes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pod
|
||||
- pods/log
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
7
k8s/operator-console/base/console-configmap.yaml
Normal file
7
k8s/operator-console/base/console-configmap.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: console-env
|
||||
data:
|
||||
CONSOLE_PORT: "9090"
|
||||
CONSOLE_TLS_PORT: "9443"
|
||||
29
k8s/operator-console/base/console-deployment.yaml
Normal file
29
k8s/operator-console/base/console-deployment.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: console
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: console
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: console
|
||||
spec:
|
||||
serviceAccountName: console-sa
|
||||
containers:
|
||||
- name: console
|
||||
image: minio/console:v0.9.7
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
env:
|
||||
- name: CONSOLE_OPERATOR_MODE
|
||||
value: "on"
|
||||
args:
|
||||
- server
|
||||
ports:
|
||||
- containerPort: 9090
|
||||
name: http
|
||||
- containerPort: 9433
|
||||
name: https
|
||||
5
k8s/operator-console/base/console-service-account.yaml
Normal file
5
k8s/operator-console/base/console-service-account.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: console-sa
|
||||
namespace: default
|
||||
14
k8s/operator-console/base/console-service.yaml
Normal file
14
k8s/operator-console/base/console-service.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: console
|
||||
labels:
|
||||
name: console
|
||||
spec:
|
||||
ports:
|
||||
- port: 9090
|
||||
name: http
|
||||
- port: 9443
|
||||
name: https
|
||||
selector:
|
||||
app: console
|
||||
10
k8s/operator-console/base/kustomization.yaml
Normal file
10
k8s/operator-console/base/kustomization.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
# beginning of customizations
|
||||
resources:
|
||||
- console-service-account.yaml
|
||||
- console-cluster-role.yaml
|
||||
- console-cluster-role-binding.yaml
|
||||
- console-configmap.yaml
|
||||
- console-service.yaml
|
||||
- console-deployment.yaml
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user