Compare commits

..

2 Commits

Author SHA1 Message Date
Daniel Valdivia
219337ff78 assets
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-09-04 22:21:24 -07:00
Daniel Valdivia
f02609b3b6 Release v0.9.7
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-09-04 21:38:49 -07:00
1795 changed files with 200170 additions and 231767 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
dist/
target/
console
!console/
portal-ui/node_modules/
.git/

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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 }}

File diff suppressed because it is too large Load Diff

34
.github/workflows/lint.yml vendored Normal file
View 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
View 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

View File

@@ -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
View File

@@ -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/

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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
.nvmrc
View File

@@ -1 +0,0 @@
18

View File

@@ -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

View File

@@ -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

View File

@@ -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).

23245
CREDITS

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View 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
View 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
View File

@@ -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
View File

@@ -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
View File

@@ -4,28 +4,61 @@
A graphical user interface for [MinIO](https://github.com/minio/minio)
| Object Browser | Dashboard | Creating a bucket |
|------------------------------------|-------------------------------|-------------------------------|
| ![Object Browser](images/pic3.png) | ![Dashboard](images/pic1.png) | ![Dashboard](images/pic2.png) |
| Dashboard | Creating a bucket |
| ------------- | ------------- |
| ![Dashboard](images/pic1.png) | ![Dashboard](images/pic2.png) |
<!-- 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)

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
})
}
}

View File

@@ -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)
})
}
}

View File

@@ -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 := &notFoundRedirectRespWr{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)
})
}

View File

@@ -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()")
})
}
}

View File

@@ -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...)
}

View File

@@ -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)
})
}
}

View File

@@ -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)
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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),
}
}

View File

@@ -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, &params.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
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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()
}
})
}
}

View File

@@ -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
}

View File

@@ -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")
}
})
}
}

View File

@@ -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()
}

View File

@@ -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
View 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
View 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
View 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
View 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)
}

View File

@@ -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
}

View File

@@ -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
View 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()
}

View File

@@ -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()
}

View File

@@ -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 {

View 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
```

View File

@@ -1,5 +0,0 @@
module.exports = {
hooks: {
onInsertPathParam: (paramName) => `encodeURIComponent(${paramName})`,
},
};

178
go.mod
View File

@@ -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

2079
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -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 \

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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)
}
})
}
}

View File

@@ -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))
}
})
}
}

View File

@@ -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
View 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
View 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
View 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"

View 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

View 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

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: console-env
data:
CONSOLE_PORT: "9090"
CONSOLE_TLS_PORT: "9443"

View 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

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: console-sa
namespace: default

View 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

View 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