Compare commits

..

7 Commits

Author SHA1 Message Date
Harshavardhana
7fa3279a93 update all other dependent packages 2020-07-25 12:34:00 -07:00
Daniel Valdivia
b631ab38d0 Operator docker iamge 2020-07-25 12:07:22 -07:00
Daniel Valdivia
16c25c0880 Go mod with v3 tag for operator 2020-07-25 12:03:16 -07:00
Daniel Valdivia
5dbdf35295 Changes to operator v3 2020-07-25 12:01:24 -07:00
Daniel Valdivia
5ecb311f79 Update Operator References 2020-07-25 11:51:29 -07:00
Daniel Valdivia
e432183bd6 Update Service Account 2020-07-25 11:19:58 -07:00
Daniel Valdivia
008075133a Upgrade Operator to 3.0.0 2020-07-24 18:34:21 -07:00
2081 changed files with 91766 additions and 361730 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
target/
mcs
!mcs/
portal-ui/node_modules/

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`):

52
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 19 * * 0'
jobs:
CodeQL-Build:
# CodeQL runs on ubuntu-latest and windows-latest
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

36
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Go
on:
pull_request:
branches:
- master
push:
branches:
- master
jobs:
build:
name: Test on Go ${{ matrix.go-version }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.13.x, 1.14.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
make test
make mcs

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

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@v3
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22
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.22 ]
os: [ ubuntu-latest ]
steps:
- name: Check out code
uses: actions/checkout@v3
- 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

22
.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,16 +19,11 @@ vendor/
# Ignore executables
target/
!pkg/logger/target/
console
!console/
mcs
!mcs/
dist/
# Ignore node_modules
web-app/node_modules/
# Ignore tls cert and key
private.key
public.crt
@@ -46,5 +31,4 @@ public.crt
# Ignore VsCode files
.vscode/
*.code-workspace
*~
.eslintcache
*~

View File

@@ -5,45 +5,24 @@ linters-settings:
misspell:
locale: US
goheader:
values:
regexp:
copyright-holder: Copyright \(c\) (20\d\d\-20\d\d)|2021|({{year}})
template-path: .license.tmpl
linters:
disable-all: true
enable:
- typecheck
- goimports
- misspell
- govet
- revive
- golint
- ineffassign
- gosimple
- gomodguard
- gofmt
- deadcode
- unparam
- unused
- staticcheck
- unconvert
- gocritic
- gofumpt
- durationcheck
- structcheck
service:
golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly
golangci-lint-version: 1.21.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
- pkg/apis/networking.gke.io
- api/operations

75
.goreleaser.yml Normal file
View File

@@ -0,0 +1,75 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
project_name: mcs
before:
hooks:
# you may remove this if you don't use vgo
- go mod tidy
builds:
-
goos:
- freebsd
- windows
- linux
- darwin
goarch:
- amd64
- arm64
env:
- CGO_ENABLED=0
main: ./cmd/mcs/
flags:
- -trimpath
- --tags=kqueue
ldflags:
- -s -w -X github.com/minio/mcs/pkg.ReleaseTag={{.Tag}} -X github.com/minio/mcs/pkg.CommitID={{.FullCommit}} -X github.com/minio/mcs/pkg.Version={{.Version}} -X github.com/minio/mcs/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/mcs/pkg.ReleaseTime={{.Date}}
archives:
-
replacements:
darwin: Darwin
linux: Linux
windows: Windows
freebsd: FreeBSD
amd64: x86_64
format_overrides:
- goos: windows
format: zip
files:
- README.md
- LICENSE
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: 'snapshot-{{ time "2006-01-02" }}'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
nfpms:
-
vendor: MinIO Inc.
homepage: https://github.com/minio/mcs
maintainer: MinIO <minio@minio.io>
description: MinIO Console Server
license: GNU Affero General Public License v3.0
formats:
- deb
- rpm
replacements:
darwin: Darwin
linux: Linux
freebsd: FreeBSD
amd64: x86_64
dockers:
-
# GOOS of the built binary that should be used.
goos: linux
# GOARCH of the built binary that should be used.
goarch: amd64
dockerfile: Dockerfile.release
image_templates:
- "minio/mcs:{{ .Tag }}"
- "minio/mcs:latest"

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,400 +0,0 @@
<!-- @format -->
# Changelog
## 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_mcs.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?
### How does ``mcs`` 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).
### What are the coding guidelines for mcs?
``mcs`` 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).

22411
CREDITS

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,4 @@
# 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
# LDAP authentication with MCS
## Setup
@@ -90,12 +11,44 @@ $ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container --detach os
Run the `billy.ldif` file using `ldapadd` command to create a new user and assign it to a group.
```
$ docker cp console/docs/ldap/billy.ldif my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ 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
$ cat > billy.ldif << EOF
# LDIF fragment to create group branch under root
dn: uid=billy,dc=example,dc=org
uid: billy
cn: billy
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/billy
uidNumber: 14583102
gidNumber: 14564100
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
mail: billy@example.org
gecos: Billy User
# Create base group
dn: ou=groups,dc=example,dc=org
objectclass:organizationalunit
ou: groups
description: generic groups branch
# create mcsAdmin group (this already exists on minio and have a policy of s3::*)
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
objectClass: top
objectClass: posixGroup
gidNumber: 678
# Assing group to new user
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
changetype: modify
add: memberuid
memberuid: billy
EOF
$ docker cp billy.ldif my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ 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 -ZZ
```
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 mcsAdmin group, you should get a list
containing ldap users and groups.
```
@@ -110,7 +63,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
@@ -120,10 +73,9 @@ Re-enter new password:
Enter LDAP Password:
```
### Add the consoleAdmin policy to user billy on MinIO
### Add the mcsAdmin policy to user billy on MinIO
```
$ cat > consoleAdmin.json << EOF
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [
@@ -147,8 +99,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 mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=billy
```
## Run MinIO
@@ -164,9 +116,12 @@ export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
./minio server ~/Data
```
## Run Console
## Run MCS
```
export CONSOLE_LDAP_ENABLED=on
./console server
export MCS_ACCESS_KEY=minio
export MCS_SECRET_KEY=minio123
...
export MCS_LDAP_ENABLED=on
./mcs server
```

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM golang:1.13
RUN apt-get update -y && apt-get install -y ca-certificates
ADD go.mod /go/src/github.com/minio/mcs/go.mod
ADD go.sum /go/src/github.com/minio/mcs/go.sum
WORKDIR /go/src/github.com/minio/mcs/
# Get dependencies - will also be cached if we won't change mod/sum
RUN go mod download
ADD . /go/src/github.com/minio/mcs/
WORKDIR /go/src/github.com/minio/mcs/
ENV CGO_ENABLED=0
RUN go build -ldflags "-w -s" -a -o mcs ./cmd/mcs
FROM scratch
MAINTAINER MinIO Development "dev@min.io"
EXPOSE 9090
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=0 /go/src/github.com/minio/mcs/mcs .
CMD ["/mcs"]

6
Dockerfile.release Normal file
View File

@@ -0,0 +1,6 @@
FROM scratch
MAINTAINER MinIO Development "dev@min.io"
EXPOSE 9090
COPY mcs /mcs
ENTRYPOINT ["/mcs"]

284
Makefile
View File

@@ -3,290 +3,62 @@ GOPATH := $(shell go env GOPATH)
# Sets the build version based on the output of the following command, if we are building for a tag, that's the build else it uses the current git branch as the build
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)
TAG ?= "minio/m3:$(VERSION)-dev"
default: console
default: mcs
.PHONY: console
console:
@echo "Building Console binary to './console'"
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o console ./cmd/console)
.PHONY: mcs
mcs:
@echo "Building mcs binary to './mcs'"
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
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 mcs 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.27.0)
verifiers: getdeps fmt lint
fmt:
@echo "Running $@ check"
@(env bash $(PWD)/verify-gofmt.sh)
crosscompile:
@(env bash $(PWD)/cross-compile.sh $(arg1))
@GO111MODULE=on gofmt -d cmd/
@GO111MODULE=on gofmt -d pkg/
lint:
@echo "Running $@ check"
@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: mcs
@echo "Installing mcs binary to '$(GOPATH)/bin/mcs'"
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/mcs $(GOPATH)/bin/mcs
@echo "Installation successful. To learn more, try \"mcs --help\"."
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
@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
swagger-console:
swagger-gen:
@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 "Generating typescript api"
@npx swagger-typescript-api -p ./swagger.yml -o ./web-app/src/api -n consoleApi.ts --custom-config generator.config.js
@git restore api/server.go
@rm -rf models
@rm -rf restapi/operations
@swagger generate server -A mcs --main-package=mcs --exclude-main -P models.Principal -f ./swagger.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 . --loglevel warn; cd ..)
@(cd portal-ui; yarn install; make build-static; 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-5:
@(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-5/")
@(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-permissions-7:
@(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-7/")
@(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)
# 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/mcs/restapi/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/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/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
clean:
@echo "Cleaning up all the generated files"
@find . -name '*.test' | xargs rm -fv
@find . -name '*~' | xargs rm -fv
@rm -vf console
@rm -vf mcs
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) 2020 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

251
README.md
View File

@@ -1,64 +1,28 @@
# MinIO Console
![build](https://github.com/minio/console/workflows/Go/badge.svg) ![license](https://img.shields.io/badge/license-AGPL%20V3-blue)
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) |
<!-- 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)
- [Contribute to console Project](#contribute-to-console-project)
<!-- markdown-toc end -->
## Install
MinIO Console is a library that provides a management and browser UI overlay for the MinIO Server.
The standalone binary installation path has been removed.
In case a Console standalone binary is needed, it can be generated by building this package from source as follows:
### Build from source
> You will need a working Go environment. Therefore, please follow [How to install Go](https://golang.org/doc/install).
> Minimum version required is go1.22
```
go install github.com/minio/console/cmd/console@latest
```
| Dashboard | Adding A User |
| ------------- | ------------- |
| ![Dashboard](images/pic1.png) | ![Dashboard](images/pic2.png) |
## Setup
All `console` needs is a MinIO user with admin privileges and URL pointing to your MinIO deployment.
All `mcs` needs is a MinIO user with admin privileges and URL pointing to your MinIO deployment.
> Note: We don't recommend using MinIO's Operator Credentials
### 1. Create a user `console` using `mc`
```bash
mc admin user add myminio/
Enter Access Key: console
Enter Secret Key: xxxxxxxx
1. Create a user for `mcs` using `mc`.
```
$ set +o history
$ mc admin user add myminio mcs YOURMCSSECRET
$ set -o history
```
### 2. Create a policy for `console` with admin access to all resources (for testing)
2. Create a policy for `mcs` with access to everything (for testing and debugging)
```sh
cat > admin.json << EOF
```
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [{
@@ -81,151 +45,86 @@ cat > admin.json << EOF
]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
```
```sh
mc admin policy create myminio/ consoleAdmin admin.json
3. Set the policy for the new `mcs` user
```
$ mc admin policy set myminio mcsAdmin user=mcs
```
### 3. Set the policy for the new `console` user
```sh
mc admin policy attach myminio consoleAdmin --user=console
### Note
Additionally, you can create policies to limit the privileges for `mcs` 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
# Required to encrypt JWT payload
export CONSOLE_PBKDF_SALT=SECRET
# MinIO Endpoint
export CONSOLE_MINIO_SERVER=http://localhost:9000
```
Now start the console service.
## Run MCS server
To run the server:
```
./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
export MCS_HMAC_JWT_SECRET=YOURJWTSIGNINGSECRET
#required to encrypt jwet payload
export MCS_PBKDF_PASSPHRASE=SECRET
#required to encrypt jwet payload
export MCS_PBKDF_SALT=SECRET
export MCS_ACCESS_KEY=mcs
export MCS_SECRET_KEY=YOURMCSSECRET
export MCS_MINIO_SERVER=http://localhost:9000
./mcs server
```
By default `console` runs on port `9090` this can be changed with `--port` of your choice.
## Start Console service with TLS:
Copy your `public.crt` and `private.key` to `~/.console/certs`, then:
```sh
./console server
2021-01-19 02:36:08.893735 I | 2021/01/19 02:36:08 server.go:129: Serving console at http://[::]:9090
2021-01-19 02:36:08.893735 I | 2021/01/19 02:36:08 server.go:129: Serving console at https://[::]:9443
```
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/
├─ public.crt
├─ private.key
├─ example.com/
│ │
│ ├─ public.crt
│ └─ private.key
└─ foobar.org/
├─ public.crt
└─ private.key
...
## Connect MCS to a Minio using TLS and a self-signed certificate
```
## Connect Console to a Minio using TLS and a self-signed certificate
Copy the MinIO `ca.crt` under `~/.console/certs/CAs`, then:
```sh
export CONSOLE_MINIO_SERVER=https://localhost:9000
./console server
...
export MCS_MINIO_SERVER_TLS_ROOT_CAS=<certificate_file_name>
export MCS_MINIO_SERVER=https://localhost:9000
./mcs server
```
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)
# Contribute to mcs Project
Please follow mcs [Contributor's Guide](https://github.com/minio/mcs/blob/master/CONTRIBUTING.md)

View File

@@ -2,12 +2,12 @@
## Supported Versions
We always provide security updates for the [latest release](https://github.com/minio/console/releases/latest).
We always provide security updates for the [latest release](https://github.com/minio/mcs/releases/latest).
Whenever there is a security update you just need to upgrade to the latest version.
## Reporting a Vulnerability
All security bugs in [minio/console](https://github,com/minio/console) (or other minio/* repositories)
All security bugs in [minio/mcs](https://github,com/minio/mcs) (or other minio/* repositories)
should be reported by email to security@min.io. Your email will be acknowledged within 48 hours,
and you'll receive a more detailed response to your email within 72 hours indicating the next steps
in handling your report.
@@ -18,14 +18,13 @@ 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: daniel@min.io, cesar@min.io
- If you receive no response: dev@min.io
### Disclosure Process
MinIO Console uses the following disclosure process:
MinIO uses the following disclosure process:
1. Once the security report is received one member of the security team tries to verify and reproduce
the issue and determines the impact it has.
@@ -34,8 +33,8 @@ MinIO Console uses the following disclosure process:
3. Code is audited to find any potential similar problems.
4. Fixes are prepared for the latest release.
5. On the date that the fixes are applied a security advisory will be published on https://blog.min.io.
Please inform us in your report email whether MinIO Console should mention your contribution w.r.t. fixing
the security issue. By default MinIO Console will **not** publish this information to protect your privacy.
Please inform us in your report email whether MinIO should mention your contribution w.r.t. fixing
the security issue. By default MinIO will **not** publish this information to protect your privacy.
This process can take some time, especially when coordination is required with maintainers of other projects.
Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we

View File

@@ -1,38 +0,0 @@
## Vulnerability Management Policy
This document formally describes the process of addressing and managing a
reported vulnerability that has been found in the MinIO Console server code base,
any directly connected ecosystem component or a direct / indirect dependency
of the code base.
### Scope
The vulnerability management policy described in this document covers the
process of investigating, assessing and resolving a vulnerability report
opened by a MinIO Console employee or an external third party.
Therefore, it lists pre-conditions and actions that should be performed to
resolve and fix a reported vulnerability.
### Vulnerability Management Process
The vulnerability management process requires that the vulnerability report
contains the following information:
- The project / component that contains the reported vulnerability.
- A description of the vulnerability. In particular, the type of the
reported vulnerability and how it might be exploited. Alternatively,
a well-established vulnerability identifier, e.g. CVE number, can be
used instead.
Based on the description mentioned above, a MinIO Console engineer or security team
member investigates:
- Whether the reported vulnerability exists.
- The conditions that are required such that the vulnerability can be exploited.
- The steps required to fix the vulnerability.
In general, if the vulnerability exists in one of the MinIO Console code bases
itself - not in a code dependency - then MinIO Console will, if possible, fix
the vulnerability or implement reasonable countermeasures such that the
vulnerability cannot be exploited anymore.

View File

@@ -1,97 +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"
"net/http"
"strings"
"testing"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations/system"
"github.com/minio/console/models"
"github.com/go-openapi/loads"
"github.com/minio/console/api/operations"
"github.com/minio/madmin-go/v3"
asrt "github.com/stretchr/testify/assert"
)
func TestArnsList(t *testing.T) {
assert := asrt.New(t)
adminClient := AdminClientMock{}
// Test-1 : getArns() returns proper arn list
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
return madmin.InfoMessage{
SQSARN: []string{"uno"},
}, nil
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
arnsList, err := getArns(ctx, adminClient)
assert.NotNil(arnsList, "arn list was returned nil")
if arnsList != nil {
assert.Equal(len(arnsList.Arns), 1, "Incorrect arns count")
}
assert.Nil(err, "Error should have been nil")
// Test-2 : getArns(ctx) fails for whatever reason
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
return madmin.InfoMessage{}, errors.New("some reason")
}
arnsList, err = getArns(ctx, adminClient)
assert.Nil(arnsList, "arn list was not returned nil")
assert.NotNil(err, "An error should have been returned")
}
func TestRegisterAdminArnsHandlers(t *testing.T) {
assert := asrt.New(t)
swaggerSpec, err := loads.Embedded(SwaggerJSON, FlatSwaggerJSON)
if err != nil {
assert.Fail("Error")
}
api := operations.NewConsoleAPI(swaggerSpec)
api.SystemArnListHandler = nil
registerAdminArnsHandlers(api)
if api.SystemArnListHandler == nil {
assert.Fail("Assignment should happen")
} else {
fmt.Println("Function got assigned: ", api.SystemArnListHandler)
}
// To test error case in registerAdminArnsHandlers
request, _ := http.NewRequest(
"GET",
"http://localhost:9090/api/v1/buckets/",
nil,
)
ArnListParamsStruct := system.ArnListParams{
HTTPRequest: request,
}
modelsPrincipal := models.Principal{
STSAccessKeyID: "accesskey",
}
var value middleware.Responder = api.SystemArnListHandler.Handle(ArnListParamsStruct, &modelsPrincipal)
str := fmt.Sprintf("%#v", value)
fmt.Println("value: ", str)
assert.Equal(strings.Contains(str, "_statusCode:500"), true)
}

View File

@@ -1,355 +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"
"io"
"time"
"github.com/minio/madmin-go/v3"
iampolicy "github.com/minio/pkg/v3/policy"
)
type AdminClientMock struct{}
var (
MinioServerInfoMock func(ctx context.Context) (madmin.InfoMessage, error)
minioChangePasswordMock func(ctx context.Context, accessKey, secretKey string) error
minioHelpConfigKVMock func(subSys, key string, envOnly bool) (madmin.Help, error)
minioGetConfigKVMock func(key string) ([]byte, error)
minioSetConfigKVMock func(kv string) (restart bool, err error)
minioDelConfigKVMock func(name string) (err error)
minioHelpConfigKVGlobalMock func(envOnly bool) (madmin.Help, error)
minioGetLogsMock func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
minioListGroupsMock func() ([]string, error)
minioUpdateGroupMembersMock func(madmin.GroupAddRemove) error
minioGetGroupDescriptionMock func(group string) (*madmin.GroupDesc, error)
minioSetGroupStatusMock func(group string, status madmin.GroupStatus) error
minioHealMock func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
minioServerHealthInfoMock func(ctx context.Context, deadline time.Duration) (interface{}, string, error)
minioListPoliciesMock func() (map[string]*iampolicy.Policy, error)
minioGetPolicyMock func(name string) (*iampolicy.Policy, error)
minioRemovePolicyMock func(name string) error
minioAddPolicyMock func(name string, policy *iampolicy.Policy) error
minioSetPolicyMock func(policyName, entityName string, isGroup bool) error
minioStartProfiling func(profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
minioStopProfiling func() (io.ReadCloser, error)
minioServiceRestartMock func(ctx context.Context) error
getSiteReplicationInfo func(ctx context.Context) (*madmin.SiteReplicationInfo, error)
addSiteReplicationInfo func(ctx context.Context, sites []madmin.PeerSite) (*madmin.ReplicateAddStatus, error)
editSiteReplicationInfo func(ctx context.Context, site madmin.PeerInfo) (*madmin.ReplicateEditStatus, error)
deleteSiteReplicationInfoMock func(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error)
getSiteReplicationStatus func(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error)
minioListTiersMock func(ctx context.Context) ([]*madmin.TierConfig, error)
minioTierStatsMock func(ctx context.Context) ([]madmin.TierInfo, error)
minioAddTiersMock func(ctx context.Context, tier *madmin.TierConfig) error
minioRemoveTierMock func(ctx context.Context, tierName string) error
minioEditTiersMock func(ctx context.Context, tierName string, creds madmin.TierCreds) error
minioVerifyTierStatusMock func(ctx context.Context, tierName string) error
minioServiceTraceMock func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
minioListUsersMock func() (map[string]madmin.UserInfo, error)
minioAddUserMock func(accessKey, secreyKey string) error
minioRemoveUserMock func(accessKey string) error
minioGetUserInfoMock func(accessKey string) (madmin.UserInfo, error)
minioSetUserStatusMock func(accessKey string, status madmin.AccountStatus) error
minioAccountInfoMock func(ctx context.Context) (madmin.AccountInfo, error)
minioAddServiceAccountMock func(ctx context.Context, policy string, user string, accessKey string, secretKey string, description string, name string, expiry *time.Time, status string) (madmin.Credentials, error)
minioListServiceAccountsMock func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
minioDeleteServiceAccountMock func(ctx context.Context, serviceAccount string) error
minioInfoServiceAccountMock func(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error)
minioUpdateServiceAccountMock func(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error
minioGetLDAPPolicyEntitiesMock func(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error)
minioListRemoteBucketsMock func(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error)
minioGetRemoteBucketMock func(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error)
minioAddRemoteBucketMock func(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error)
)
func (ac AdminClientMock) serverInfo(ctx context.Context) (madmin.InfoMessage, error) {
return MinioServerInfoMock(ctx)
}
func (ac AdminClientMock) listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error) {
return minioListRemoteBucketsMock(ctx, bucket, arnType)
}
func (ac AdminClientMock) getRemoteBucket(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error) {
return minioGetRemoteBucketMock(ctx, bucket, arnType)
}
func (ac AdminClientMock) removeRemoteBucket(_ context.Context, _, _ string) error {
return nil
}
func (ac AdminClientMock) addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) {
return minioAddRemoteBucketMock(ctx, bucket, target)
}
func (ac AdminClientMock) changePassword(ctx context.Context, accessKey, secretKey string) error {
return minioChangePasswordMock(ctx, accessKey, secretKey)
}
func (ac AdminClientMock) speedtest(_ context.Context, _ madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error) {
return nil, nil
}
func (ac AdminClientMock) verifyTierStatus(ctx context.Context, tier string) error {
return minioVerifyTierStatusMock(ctx, tier)
}
// mock function helpConfigKV()
func (ac AdminClientMock) helpConfigKV(_ context.Context, subSys, key string, envOnly bool) (madmin.Help, error) {
return minioHelpConfigKVMock(subSys, key, envOnly)
}
// mock function getConfigKV()
func (ac AdminClientMock) getConfigKV(_ context.Context, name string) ([]byte, error) {
return minioGetConfigKVMock(name)
}
// mock function setConfigKV()
func (ac AdminClientMock) setConfigKV(_ context.Context, kv string) (restart bool, err error) {
return minioSetConfigKVMock(kv)
}
// mock function helpConfigKV()
func (ac AdminClientMock) helpConfigKVGlobal(_ context.Context, envOnly bool) (madmin.Help, error) {
return minioHelpConfigKVGlobalMock(envOnly)
}
func (ac AdminClientMock) delConfigKV(_ context.Context, name string) (err error) {
return minioDelConfigKVMock(name)
}
func (ac AdminClientMock) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return minioGetLogsMock(ctx, node, lineCnt, logKind)
}
func (ac AdminClientMock) listGroups(_ context.Context) ([]string, error) {
return minioListGroupsMock()
}
func (ac AdminClientMock) updateGroupMembers(_ context.Context, req madmin.GroupAddRemove) error {
return minioUpdateGroupMembersMock(req)
}
func (ac AdminClientMock) getGroupDescription(_ context.Context, group string) (*madmin.GroupDesc, error) {
return minioGetGroupDescriptionMock(group)
}
func (ac AdminClientMock) setGroupStatus(_ context.Context, group string, status madmin.GroupStatus) error {
return minioSetGroupStatusMock(group, status)
}
func (ac AdminClientMock) heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool,
) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return minioHealMock(ctx, bucket, prefix, healOpts, clientToken, forceStart, forceStop)
}
func (ac AdminClientMock) serverHealthInfo(ctx context.Context, deadline time.Duration) (interface{}, string, error) {
return minioServerHealthInfoMock(ctx, deadline)
}
func (ac AdminClientMock) addOrUpdateIDPConfig(_ context.Context, _, _, _ string, _ bool) (restart bool, err error) {
return true, nil
}
func (ac AdminClientMock) listIDPConfig(_ context.Context, _ string) ([]madmin.IDPListItem, error) {
return []madmin.IDPListItem{{Name: "mock"}}, nil
}
func (ac AdminClientMock) deleteIDPConfig(_ context.Context, _, _ string) (restart bool, err error) {
return true, nil
}
func (ac AdminClientMock) getIDPConfig(_ context.Context, _, _ string) (c madmin.IDPConfig, err error) {
return madmin.IDPConfig{Info: []madmin.IDPCfgInfo{{Key: "mock", Value: "mock"}}}, nil
}
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) kmsAPIs(_ context.Context) ([]madmin.KMSAPI, error) {
return []madmin.KMSAPI{{Method: "GET", Path: "/mock"}}, nil
}
func (ac AdminClientMock) kmsMetrics(_ context.Context) (*madmin.KMSMetrics, error) {
return &madmin.KMSMetrics{}, nil
}
func (ac AdminClientMock) kmsVersion(_ context.Context) (*madmin.KMSVersion, error) {
return &madmin.KMSVersion{Version: "test-version"}, nil
}
func (ac AdminClientMock) createKey(_ context.Context, _ string) error {
return nil
}
func (ac AdminClientMock) listKeys(_ context.Context, _ string) ([]madmin.KMSKeyInfo, error) {
return []madmin.KMSKeyInfo{{
Name: "name",
CreatedBy: "by",
}}, nil
}
func (ac AdminClientMock) keyStatus(_ context.Context, _ string) (*madmin.KMSKeyStatus, error) {
return &madmin.KMSKeyStatus{KeyID: "key"}, nil
}
func (ac AdminClientMock) listPolicies(_ context.Context) (map[string]*iampolicy.Policy, error) {
return minioListPoliciesMock()
}
func (ac AdminClientMock) getPolicy(_ context.Context, name string) (*iampolicy.Policy, error) {
return minioGetPolicyMock(name)
}
func (ac AdminClientMock) removePolicy(_ context.Context, name string) error {
return minioRemovePolicyMock(name)
}
func (ac AdminClientMock) addPolicy(_ context.Context, name string, policy *iampolicy.Policy) error {
return minioAddPolicyMock(name, policy)
}
func (ac AdminClientMock) setPolicy(_ context.Context, policyName, entityName string, isGroup bool) error {
return minioSetPolicyMock(policyName, entityName, isGroup)
}
// mock function for startProfiling()
func (ac AdminClientMock) startProfiling(_ context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
return minioStartProfiling(profiler)
}
// mock function for stopProfiling()
func (ac AdminClientMock) stopProfiling(_ context.Context) (io.ReadCloser, error) {
return minioStopProfiling()
}
// mock function of serviceRestart()
func (ac AdminClientMock) serviceRestart(ctx context.Context) error {
return minioServiceRestartMock(ctx)
}
func (ac AdminClientMock) getSiteReplicationInfo(ctx context.Context) (*madmin.SiteReplicationInfo, error) {
return getSiteReplicationInfo(ctx)
}
func (ac AdminClientMock) addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite, _ madmin.SRAddOptions) (*madmin.ReplicateAddStatus, error) {
return addSiteReplicationInfo(ctx, sites)
}
func (ac AdminClientMock) editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo, _ madmin.SREditOptions) (*madmin.ReplicateEditStatus, error) {
return editSiteReplicationInfo(ctx, site)
}
func (ac AdminClientMock) deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error) {
return deleteSiteReplicationInfoMock(ctx, removeReq)
}
func (ac AdminClientMock) getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error) {
return getSiteReplicationStatus(ctx, params)
}
func (ac AdminClientMock) listTiers(ctx context.Context) ([]*madmin.TierConfig, error) {
return minioListTiersMock(ctx)
}
func (ac AdminClientMock) tierStats(ctx context.Context) ([]madmin.TierInfo, error) {
return minioTierStatsMock(ctx)
}
func (ac AdminClientMock) addTier(ctx context.Context, tier *madmin.TierConfig) error {
return minioAddTiersMock(ctx, tier)
}
func (ac AdminClientMock) removeTier(ctx context.Context, tierName string) error {
return minioRemoveTierMock(ctx, tierName)
}
func (ac AdminClientMock) editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error {
return minioEditTiersMock(ctx, tierName, creds)
}
func (ac AdminClientMock) serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
return minioServiceTraceMock(ctx, threshold, s3, internal, storage, os, errTrace)
}
func (ac AdminClientMock) listUsers(_ context.Context) (map[string]madmin.UserInfo, error) {
return minioListUsersMock()
}
func (ac AdminClientMock) addUser(_ context.Context, accessKey, secretKey string) error {
return minioAddUserMock(accessKey, secretKey)
}
func (ac AdminClientMock) removeUser(_ context.Context, accessKey string) error {
return minioRemoveUserMock(accessKey)
}
func (ac AdminClientMock) getUserInfo(_ context.Context, accessKey string) (madmin.UserInfo, error) {
return minioGetUserInfoMock(accessKey)
}
func (ac AdminClientMock) setUserStatus(_ context.Context, accessKey string, status madmin.AccountStatus) error {
return minioSetUserStatusMock(accessKey, status)
}
func (ac AdminClientMock) AccountInfo(ctx context.Context) (madmin.AccountInfo, error) {
return minioAccountInfoMock(ctx)
}
func (ac AdminClientMock) addServiceAccount(ctx context.Context, policy string, user string, accessKey string, secretKey string, description string, name string, expiry *time.Time, status string) (madmin.Credentials, error) {
return minioAddServiceAccountMock(ctx, policy, user, accessKey, secretKey, description, name, expiry, status)
}
func (ac AdminClientMock) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
return minioListServiceAccountsMock(ctx, user)
}
func (ac AdminClientMock) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
return minioDeleteServiceAccountMock(ctx, serviceAccount)
}
func (ac AdminClientMock) infoServiceAccount(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error) {
return minioInfoServiceAccountMock(ctx, serviceAccount)
}
func (ac AdminClientMock) updateServiceAccount(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error {
return minioUpdateServiceAccountMock(ctx, serviceAccount, opts)
}
func (ac AdminClientMock) getLDAPPolicyEntities(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
return minioGetLDAPPolicyEntitiesMock(ctx, query)
}

View File

@@ -1,316 +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"
"fmt"
"strings"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
"github.com/minio/console/api/operations"
"github.com/minio/console/models"
madmin "github.com/minio/madmin-go/v3"
cfgApi "github.com/minio/console/api/operations/configuration"
)
func registerConfigHandlers(api *operations.ConsoleAPI) {
// List Configurations
api.ConfigurationListConfigHandler = cfgApi.ListConfigHandlerFunc(func(params cfgApi.ListConfigParams, session *models.Principal) middleware.Responder {
configListResp, err := getListConfigResponse(session, params)
if err != nil {
return cfgApi.NewListConfigDefault(err.Code).WithPayload(err.APIError)
}
return cfgApi.NewListConfigOK().WithPayload(configListResp)
})
// Configuration Info
api.ConfigurationConfigInfoHandler = cfgApi.ConfigInfoHandlerFunc(func(params cfgApi.ConfigInfoParams, session *models.Principal) middleware.Responder {
config, err := getConfigResponse(session, params)
if err != nil {
return cfgApi.NewConfigInfoDefault(err.Code).WithPayload(err.APIError)
}
return cfgApi.NewConfigInfoOK().WithPayload(config)
})
// Set Configuration
api.ConfigurationSetConfigHandler = cfgApi.SetConfigHandlerFunc(func(params cfgApi.SetConfigParams, session *models.Principal) middleware.Responder {
resp, err := setConfigResponse(session, params)
if err != nil {
return cfgApi.NewSetConfigDefault(err.Code).WithPayload(err.APIError)
}
return cfgApi.NewSetConfigOK().WithPayload(resp)
})
// Reset Configuration
api.ConfigurationResetConfigHandler = cfgApi.ResetConfigHandlerFunc(func(params cfgApi.ResetConfigParams, session *models.Principal) middleware.Responder {
resp, err := resetConfigResponse(session, params)
if err != nil {
return cfgApi.NewResetConfigDefault(err.Code).WithPayload(err.APIError)
}
return cfgApi.NewResetConfigOK().WithPayload(resp)
})
// Export Configuration as base64 string.
api.ConfigurationExportConfigHandler = cfgApi.ExportConfigHandlerFunc(func(params cfgApi.ExportConfigParams, session *models.Principal) middleware.Responder {
resp, err := exportConfigResponse(session, params)
if err != nil {
return cfgApi.NewExportConfigDefault(err.Code).WithPayload(err.APIError)
}
return cfgApi.NewExportConfigOK().WithPayload(resp)
})
api.ConfigurationPostConfigsImportHandler = cfgApi.PostConfigsImportHandlerFunc(func(params cfgApi.PostConfigsImportParams, session *models.Principal) middleware.Responder {
_, err := importConfigResponse(session, params)
if err != nil {
return cfgApi.NewPostConfigsImportDefault(err.Code).WithPayload(err.APIError)
}
return cfgApi.NewPostConfigsImportDefault(200)
})
}
// listConfig gets all configurations' names and their descriptions
func listConfig(client MinioAdmin) ([]*models.ConfigDescription, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
configKeysHelp, err := client.helpConfigKV(ctx, "", "", false)
if err != nil {
return nil, err
}
var configDescs []*models.ConfigDescription
for _, c := range configKeysHelp.KeysHelp {
desc := &models.ConfigDescription{
Key: c.Key,
Description: c.Description,
}
configDescs = append(configDescs, desc)
}
return configDescs, nil
}
// getListConfigResponse performs listConfig() and serializes it to the handler's output
func getListConfigResponse(session *models.Principal, params cfgApi.ListConfigParams) (*models.ListConfigResponse, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
configDescs, err := listConfig(adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
listGroupsResponse := &models.ListConfigResponse{
Configurations: configDescs,
Total: int64(len(configDescs)),
}
return listGroupsResponse, nil
}
// getConfig gets the key values for a defined configuration.
func getConfig(ctx context.Context, client MinioAdmin, name string) ([]*models.Configuration, error) {
configBytes, err := client.getConfigKV(ctx, name)
if err != nil {
return nil, err
}
subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
if err != nil {
return nil, err
}
var configSubSysList []*models.Configuration
for _, scfg := range subSysConfigs {
if !madmin.SubSystems.Contains(scfg.SubSystem) {
return nil, fmt.Errorf("no sub-systems found")
}
var confkv []*models.ConfigurationKV
for _, kv := range scfg.KV {
var envOverride *models.EnvOverride
if kv.EnvOverride != nil {
envOverride = &models.EnvOverride{
Name: kv.EnvOverride.Name,
Value: kv.EnvOverride.Value,
}
}
confkv = append(confkv, &models.ConfigurationKV{Key: kv.Key, Value: kv.Value, EnvOverride: envOverride})
}
if len(confkv) == 0 {
continue
}
var fullConfigName string
if scfg.Target == "" {
fullConfigName = scfg.SubSystem
} else {
fullConfigName = scfg.SubSystem + ":" + scfg.Target
}
configSubSysList = append(configSubSysList, &models.Configuration{KeyValues: confkv, Name: fullConfigName})
}
return configSubSysList, nil
}
// getConfigResponse performs getConfig() and serializes it to the handler's output
func getConfigResponse(session *models.Principal, params cfgApi.ConfigInfoParams) ([]*models.Configuration, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
configurations, err := getConfig(ctx, adminClient, params.Name)
if err != nil {
errorVal := ErrorWithContext(ctx, err)
minioError := madmin.ToErrorResponse(err)
if minioError.Code == "XMinioConfigError" {
errorVal.Code = 404
}
return nil, errorVal
}
return configurations, nil
}
// setConfig sets a configuration with the defined key values
func setConfig(ctx context.Context, client MinioAdmin, configName *string, kvs []*models.ConfigurationKV) (restart bool, err error) {
config := buildConfig(configName, kvs)
restart, err = client.setConfigKV(ctx, *config)
if err != nil {
return false, err
}
return restart, nil
}
func setConfigWithARNAccountID(ctx context.Context, client MinioAdmin, configName *string, kvs []*models.ConfigurationKV, arnAccountID string) (restart bool, err error) {
// if arnAccountID is not empty the configuration will be treated as a notification target
// arnAccountID will be used as an identifier for that specific target
// docs: https://min.io/docs/minio/linux/administration/monitoring/bucket-notifications.html
if arnAccountID != "" {
configName = swag.String(fmt.Sprintf("%s:%s", *configName, arnAccountID))
}
return setConfig(ctx, client, configName, kvs)
}
// buildConfig builds a concatenated string including name and keyvalues
// e.g. `region name=us-west-1`
func buildConfig(configName *string, kvs []*models.ConfigurationKV) *string {
var builder strings.Builder
builder.WriteString(*configName)
for _, kv := range kvs {
key := strings.TrimSpace(kv.Key)
if key == "" {
continue
}
builder.WriteString(" ")
builder.WriteString(key)
builder.WriteString("=")
// All newlines must be converted to ','
builder.WriteString(strings.ReplaceAll(strings.TrimSpace(fmt.Sprintf("\"%s\"", kv.Value)), "\n", ","))
}
config := builder.String()
return &config
}
// setConfigResponse implements setConfig() to be used by handler
func setConfigResponse(session *models.Principal, params cfgApi.SetConfigParams) (*models.SetConfigResponse, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
configName := params.Name
needsRestart, err := setConfigWithARNAccountID(ctx, adminClient, &configName, params.Body.KeyValues, params.Body.ArnResourceID)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SetConfigResponse{Restart: needsRestart}, nil
}
func resetConfig(ctx context.Context, client MinioAdmin, configName *string) (err error) {
err = client.delConfigKV(ctx, *configName)
return err
}
// resetConfigResponse implements resetConfig() to be used by handler
func resetConfigResponse(session *models.Principal, params cfgApi.ResetConfigParams) (*models.SetConfigResponse, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
err = resetConfig(ctx, adminClient, &params.Name)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SetConfigResponse{Restart: true}, nil
}
func exportConfigResponse(session *models.Principal, params cfgApi.ExportConfigParams) (*models.ConfigExportResponse, *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)
}
configRes, err := mAdmin.GetConfig(ctx)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// may contain sensitive information so unpack only when required.
return &models.ConfigExportResponse{
Status: "success",
Value: base64.StdEncoding.EncodeToString(configRes),
}, nil
}
func importConfigResponse(session *models.Principal, params cfgApi.PostConfigsImportParams) (*cfgApi.PostConfigsImportDefault, *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)
}
file, _, err := params.HTTPRequest.FormFile("file")
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
defer file.Close()
err = mAdmin.SetConfig(ctx, file)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &cfgApi.PostConfigsImportDefault{}, nil
}

View File

@@ -1,153 +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"
b64 "encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"time"
"github.com/minio/console/pkg/logger"
"github.com/minio/console/pkg/utils"
subnet "github.com/minio/console/pkg/subnet"
mc "github.com/minio/mc/cmd"
"github.com/minio/websocket"
)
// startHealthInfo starts fetching mc.ServerHealthInfo and
// sends messages with the corresponding data on the websocket connection
func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadline *time.Duration) error {
if deadline == nil {
return errors.New("duration can't be nil on startHealthInfo")
}
// Fetch info of all servers (cluster or single server)
healthInfo, version, err := client.serverHealthInfo(ctx, *deadline)
if err != nil {
return err
}
compressedDiag, err := mc.TarGZHealthInfo(healthInfo, version)
if err != nil {
return err
}
encodedDiag := b64.StdEncoding.EncodeToString(compressedDiag)
type messageReport struct {
Encoded string `json:"encoded"`
ServerHealthInfo interface{} `json:"serverHealthInfo"`
SubnetResponse string `json:"subnetResponse"`
}
ctx = context.WithValue(ctx, utils.ContextClientIP, conn.remoteAddress())
err = sendHealthInfoToSubnet(ctx, compressedDiag, client)
report := messageReport{
Encoded: encodedDiag,
ServerHealthInfo: healthInfo,
SubnetResponse: mc.SubnetBaseURL() + "/health",
}
if err != nil {
report.SubnetResponse = fmt.Sprintf("Error: %s", err.Error())
}
message, err := json.Marshal(report)
if err != nil {
return err
}
// Send Message through websocket connection
return conn.writeMessage(websocket.TextMessage, message)
}
// getHealthInfoOptionsFromReq gets duration for startHealthInfo request
// path come as : `/health-info?deadline=2h`
func getHealthInfoOptionsFromReq(req *http.Request) (*time.Duration, error) {
deadlineDuration, err := time.ParseDuration(req.FormValue("deadline"))
if err != nil {
return nil, err
}
return &deadlineDuration, nil
}
func updateMcGlobals(subnetTokenConfig subnet.LicenseTokenConfig) error {
mc.GlobalDevMode = getConsoleDevMode()
if len(subnetTokenConfig.Proxy) > 0 {
proxyURL, e := url.Parse(subnetTokenConfig.Proxy)
if e != nil {
return e
}
mc.GlobalSubnetProxyURL = proxyURL
}
return nil
}
func sendHealthInfoToSubnet(ctx context.Context, compressedHealthInfo []byte, client MinioAdmin) error {
filename := fmt.Sprintf("health_%d.json.gz", time.Now().Unix())
subnetTokenConfig, e := GetSubnetKeyFromMinIOConfig(ctx, client)
if e != nil {
return e
}
e = updateMcGlobals(*subnetTokenConfig)
if e != nil {
return e
}
var apiKey string
if len(subnetTokenConfig.APIKey) != 0 {
apiKey = subnetTokenConfig.APIKey
} else {
apiKey, e = subnet.GetSubnetAPIKeyUsingLicense(subnetTokenConfig.License)
if e != nil {
return e
}
}
e = os.WriteFile(filename, compressedHealthInfo, 0o666)
if e != nil {
return e
}
headers := mc.SubnetAPIKeyAuthHeaders(apiKey)
resp, e := (&mc.SubnetFileUploader{
FilePath: filename,
ReqURL: mc.SubnetUploadURL("health"),
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
if e != nil {
// file gets deleted only if upload is successful
// so we delete explicitly here as we already have the bytes
logger.LogIf(ctx, os.Remove(filename))
return e
}
type SubnetResponse struct {
LicenseV2 string `json:"license_v2,omitempty"`
APIKey string `json:"api_key,omitempty"`
}
var subnetResp SubnetResponse
e = json.Unmarshal([]byte(resp), &subnetResp)
if e != nil {
return e
}
return nil
}

View File

@@ -1,147 +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"
"reflect"
"testing"
"time"
madmin "github.com/minio/madmin-go/v3"
)
func Test_serverHealthInfo(t *testing.T) {
var testReceiver chan madmin.HealthInfo
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client := AdminClientMock{}
mockWSConn := mockConn{}
deadlineDuration, _ := time.ParseDuration("1h")
type args struct {
deadline time.Duration
wsWriteMock func(messageType int, data []byte) error
mockMessages []madmin.HealthInfo
}
tests := []struct {
test string
args args
wantError error
}{
{
test: "Return simple health info, no errors",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{{}, {}},
wsWriteMock: func(_ int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
testReceiver <- t
return nil
},
},
wantError: nil,
},
{
test: "Return simple health info2, no errors",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{{}},
wsWriteMock: func(_ int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
testReceiver <- t
return nil
},
},
wantError: nil,
},
{
test: "Handle error on ws write",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{{}},
wsWriteMock: func(_ int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
return errors.New("error on write")
},
},
wantError: errors.New("error on write"),
},
{
test: "Handle error on health function",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{
{
Error: "error on healthInfo",
},
},
wsWriteMock: func(_ int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
return nil
},
},
wantError: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.test, func(_ *testing.T) {
// make testReceiver channel
testReceiver = make(chan madmin.HealthInfo, len(tt.args.mockMessages))
// mock function same for all tests, changes mockMessages
minioServerHealthInfoMock = func(_ context.Context,
_ time.Duration,
) (interface{}, string, error) {
info := tt.args.mockMessages[0]
return info, madmin.HealthInfoVersion, nil
}
connWriteMessageMock = tt.args.wsWriteMock
err := startHealthInfo(ctx, mockWSConn, client, &deadlineDuration)
// close test mock channel
close(testReceiver)
// check that the TestReceiver got the same number of data from Console.
index := 0
for info := range testReceiver {
if !reflect.DeepEqual(info, tt.args.mockMessages[index]) {
t.Errorf("startHealthInfo() got: %v, want: %v", info, tt.args.mockMessages[index])
return
}
index++
}
if !reflect.DeepEqual(err, tt.wantError) {
t.Errorf("startHealthInfo() error: %v, wantError: %v", err, tt.wantError)
return
}
})
}
}

View File

@@ -1,290 +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"
"fmt"
"time"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
"github.com/minio/console/api/operations/idp"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
)
var errInvalidIDPType = fmt.Errorf("IDP type must be one of %v", madmin.ValidIDPConfigTypes)
func registerIDPHandlers(api *operations.ConsoleAPI) {
api.IdpCreateConfigurationHandler = idp.CreateConfigurationHandlerFunc(func(params idp.CreateConfigurationParams, session *models.Principal) middleware.Responder {
response, err := createIDPConfigurationResponse(session, params)
if err != nil {
return idp.NewCreateConfigurationDefault(err.Code).WithPayload(err.APIError)
}
return idp.NewCreateConfigurationCreated().WithPayload(response)
})
api.IdpUpdateConfigurationHandler = idp.UpdateConfigurationHandlerFunc(func(params idp.UpdateConfigurationParams, session *models.Principal) middleware.Responder {
response, err := updateIDPConfigurationResponse(session, params)
if err != nil {
return idp.NewUpdateConfigurationDefault(err.Code).WithPayload(err.APIError)
}
return idp.NewUpdateConfigurationOK().WithPayload(response)
})
api.IdpListConfigurationsHandler = idp.ListConfigurationsHandlerFunc(func(params idp.ListConfigurationsParams, session *models.Principal) middleware.Responder {
response, err := listIDPConfigurationsResponse(session, params)
if err != nil {
return idp.NewListConfigurationsDefault(err.Code).WithPayload(err.APIError)
}
return idp.NewListConfigurationsOK().WithPayload(response)
})
api.IdpDeleteConfigurationHandler = idp.DeleteConfigurationHandlerFunc(func(params idp.DeleteConfigurationParams, session *models.Principal) middleware.Responder {
response, err := deleteIDPConfigurationResponse(session, params)
if err != nil {
return idp.NewDeleteConfigurationDefault(err.Code).WithPayload(err.APIError)
}
return idp.NewDeleteConfigurationOK().WithPayload(response)
})
api.IdpGetConfigurationHandler = idp.GetConfigurationHandlerFunc(func(params idp.GetConfigurationParams, session *models.Principal) middleware.Responder {
response, err := getIDPConfigurationsResponse(session, params)
if err != nil {
return idp.NewGetConfigurationDefault(err.Code).WithPayload(err.APIError)
}
return idp.NewGetConfigurationOK().WithPayload(response)
})
api.IdpGetLDAPEntitiesHandler = idp.GetLDAPEntitiesHandlerFunc(func(params idp.GetLDAPEntitiesParams, session *models.Principal) middleware.Responder {
response, err := getLDAPEntitiesResponse(session, params)
if err != nil {
return idp.NewGetLDAPEntitiesDefault(err.Code).WithPayload(err.APIError)
}
return idp.NewGetLDAPEntitiesOK().WithPayload(response)
})
}
func createIDPConfigurationResponse(session *models.Principal, params idp.CreateConfigurationParams) (*models.SetIDPResponse, *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)
}
restart, err := createOrUpdateIDPConfig(ctx, params.Type, params.Body.Name, params.Body.Input, false, AdminClient{Client: mAdmin})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SetIDPResponse{Restart: restart}, nil
}
func updateIDPConfigurationResponse(session *models.Principal, params idp.UpdateConfigurationParams) (*models.SetIDPResponse, *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)
}
restart, err := createOrUpdateIDPConfig(ctx, params.Type, params.Name, params.Body.Input, true, AdminClient{Client: mAdmin})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SetIDPResponse{Restart: restart}, nil
}
func createOrUpdateIDPConfig(ctx context.Context, idpType, name, input string, update bool, client MinioAdmin) (bool, error) {
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
return false, errInvalidIDPType
}
restart, err := client.addOrUpdateIDPConfig(ctx, idpType, name, input, update)
if err != nil {
return false, err
}
return restart, nil
}
func listIDPConfigurationsResponse(session *models.Principal, params idp.ListConfigurationsParams) (*models.IdpListConfigurationsResponse, *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)
}
results, err := listIDPConfigurations(ctx, params.Type, AdminClient{Client: mAdmin})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.IdpListConfigurationsResponse{Results: results}, nil
}
func listIDPConfigurations(ctx context.Context, idpType string, client MinioAdmin) ([]*models.IdpServerConfiguration, error) {
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
return nil, errInvalidIDPType
}
results, err := client.listIDPConfig(ctx, idpType)
if err != nil {
return nil, err
}
return parseIDPConfigurations(results), nil
}
func parseIDPConfigurations(configs []madmin.IDPListItem) (serverConfigs []*models.IdpServerConfiguration) {
for _, c := range configs {
serverConfigs = append(serverConfigs, &models.IdpServerConfiguration{
Name: c.Name,
Enabled: c.Enabled,
Type: c.Type,
})
}
return serverConfigs
}
func deleteIDPConfigurationResponse(session *models.Principal, params idp.DeleteConfigurationParams) (*models.SetIDPResponse, *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)
}
restart, err := deleteIDPConfig(ctx, params.Type, params.Name, AdminClient{Client: mAdmin})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SetIDPResponse{Restart: restart}, nil
}
func deleteIDPConfig(ctx context.Context, idpType, name string, client MinioAdmin) (bool, error) {
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
return false, errInvalidIDPType
}
restart, err := client.deleteIDPConfig(ctx, idpType, name)
if err != nil {
return false, err
}
return restart, nil
}
func getIDPConfigurationsResponse(session *models.Principal, params idp.GetConfigurationParams) (*models.IdpServerConfiguration, *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)
}
result, err := getIDPConfiguration(ctx, params.Type, params.Name, AdminClient{Client: mAdmin})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return result, nil
}
func getIDPConfiguration(ctx context.Context, idpType, name string, client MinioAdmin) (*models.IdpServerConfiguration, error) {
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
return nil, errInvalidIDPType
}
config, err := client.getIDPConfig(ctx, idpType, name)
if err != nil {
return nil, err
}
return &models.IdpServerConfiguration{
Name: config.Name,
Type: config.Type,
Info: parseIDPConfigurationsInfo(config.Info),
}, nil
}
func parseIDPConfigurationsInfo(infoList []madmin.IDPCfgInfo) (results []*models.IdpServerConfigurationInfo) {
for _, info := range infoList {
results = append(results, &models.IdpServerConfigurationInfo{
Key: info.Key,
Value: info.Value,
IsCfg: info.IsCfg,
IsEnv: info.IsEnv,
})
}
return results
}
func getLDAPEntitiesResponse(session *models.Principal, params idp.GetLDAPEntitiesParams) (*models.LdapEntities, *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)
}
result, err := getEntitiesResult(ctx, AdminClient{Client: mAdmin}, params.Body.Users, params.Body.Groups, params.Body.Policies)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return result, nil
}
func getEntitiesResult(ctx context.Context, client MinioAdmin, users, groups, policies []string) (*models.LdapEntities, error) {
entities, err := client.getLDAPPolicyEntities(ctx, madmin.PolicyEntitiesQuery{
Users: users,
Groups: groups,
Policy: policies,
})
if err != nil {
return nil, err
}
var result models.LdapEntities
var usersEntity []*models.LdapUserPolicyEntity
var groupsEntity []*models.LdapGroupPolicyEntity
var policiesEntity []*models.LdapPolicyEntity
result.Timestamp = entities.Timestamp.Format(time.RFC3339)
for _, userMapping := range entities.UserMappings {
mapItem := models.LdapUserPolicyEntity{
User: userMapping.User,
Policies: userMapping.Policies,
}
usersEntity = append(usersEntity, &mapItem)
}
result.Users = usersEntity
for _, groupsMapping := range entities.GroupMappings {
mapItem := models.LdapGroupPolicyEntity{
Group: groupsMapping.Group,
Policies: groupsMapping.Policies,
}
groupsEntity = append(groupsEntity, &mapItem)
}
result.Groups = groupsEntity
for _, policyMapping := range entities.PolicyMappings {
mapItem := models.LdapPolicyEntity{
Policy: policyMapping.Policy,
Users: policyMapping.Users,
Groups: policyMapping.Groups,
}
policiesEntity = append(policiesEntity, &mapItem)
}
result.Policies = policiesEntity
return &result, nil
}

View File

@@ -1,319 +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"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/minio/madmin-go/v3"
"github.com/minio/console/api/operations"
"github.com/minio/console/api/operations/idp"
"github.com/minio/console/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type IDPTestSuite struct {
suite.Suite
assert *assert.Assertions
currentServer string
isServerSet bool
server *httptest.Server
adminClient AdminClientMock
}
func (suite *IDPTestSuite) SetupSuite() {
suite.assert = assert.New(suite.T())
suite.adminClient = AdminClientMock{}
minioServiceRestartMock = func(_ context.Context) error {
return nil
}
}
func (suite *IDPTestSuite) SetupTest() {
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
os.Setenv(ConsoleMinIOServer, suite.server.URL)
}
func (suite *IDPTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(400)
}
func (suite *IDPTestSuite) TearDownSuite() {
}
func (suite *IDPTestSuite) TearDownTest() {
if suite.isServerSet {
os.Setenv(ConsoleMinIOServer, suite.currentServer)
} else {
os.Unsetenv(ConsoleMinIOServer)
}
}
func (suite *IDPTestSuite) TestRegisterIDPHandlers() {
api := &operations.ConsoleAPI{}
suite.assertHandlersAreNil(api)
registerIDPHandlers(api)
suite.assertHandlersAreNotNil(api)
}
func (suite *IDPTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
suite.assert.Nil(api.IdpCreateConfigurationHandler)
suite.assert.Nil(api.IdpListConfigurationsHandler)
suite.assert.Nil(api.IdpUpdateConfigurationHandler)
suite.assert.Nil(api.IdpGetConfigurationHandler)
suite.assert.Nil(api.IdpGetConfigurationHandler)
suite.assert.Nil(api.IdpDeleteConfigurationHandler)
}
func (suite *IDPTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
suite.assert.NotNil(api.IdpCreateConfigurationHandler)
suite.assert.NotNil(api.IdpListConfigurationsHandler)
suite.assert.NotNil(api.IdpUpdateConfigurationHandler)
suite.assert.NotNil(api.IdpGetConfigurationHandler)
suite.assert.NotNil(api.IdpGetConfigurationHandler)
suite.assert.NotNil(api.IdpDeleteConfigurationHandler)
}
func (suite *IDPTestSuite) TestCreateIDPConfigurationHandlerWithError() {
params, api := suite.initCreateIDPConfigurationRequest()
response := api.IdpCreateConfigurationHandler.Handle(params, &models.Principal{})
_, ok := response.(*idp.CreateConfigurationDefault)
suite.assert.True(ok)
}
func (suite *IDPTestSuite) initCreateIDPConfigurationRequest() (params idp.CreateConfigurationParams, api operations.ConsoleAPI) {
registerIDPHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Body = &models.IdpServerConfiguration{}
params.Type = "ldap"
return params, api
}
func (suite *IDPTestSuite) TestCreateIDPConfigurationWithoutError() {
ctx := context.Background()
_, err := createOrUpdateIDPConfig(ctx, "ldap", "", "", false, suite.adminClient)
suite.assert.Nil(err)
}
func (suite *IDPTestSuite) TestCreateIDPConfigurationWithWrongType() {
ctx := context.Background()
_, err := createOrUpdateIDPConfig(ctx, "", "", "", false, suite.adminClient)
suite.assert.NotNil(err)
}
func (suite *IDPTestSuite) TestUpdateIDPConfigurationHandlerWithError() {
params, api := suite.initUpdateIDPConfigurationRequest()
response := api.IdpUpdateConfigurationHandler.Handle(params, &models.Principal{})
_, ok := response.(*idp.UpdateConfigurationDefault)
suite.assert.True(ok)
}
func (suite *IDPTestSuite) initUpdateIDPConfigurationRequest() (params idp.UpdateConfigurationParams, api operations.ConsoleAPI) {
registerIDPHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Body = &models.IdpServerConfiguration{}
params.Type = "ldap"
return params, api
}
func (suite *IDPTestSuite) TestUpdateIDPConfigurationWithoutError() {
ctx := context.Background()
_, err := createOrUpdateIDPConfig(ctx, "ldap", "", "", true, suite.adminClient)
suite.assert.Nil(err)
}
func (suite *IDPTestSuite) TestUpdateIDPConfigurationWithWrongType() {
ctx := context.Background()
_, err := createOrUpdateIDPConfig(ctx, "", "", "", true, suite.adminClient)
suite.assert.NotNil(err)
}
func (suite *IDPTestSuite) TestListIDPConfigurationHandlerWithError() {
params, api := suite.initListIDPConfigurationsRequest()
response := api.IdpListConfigurationsHandler.Handle(params, &models.Principal{})
_, ok := response.(*idp.ListConfigurationsDefault)
suite.assert.True(ok)
}
func (suite *IDPTestSuite) initListIDPConfigurationsRequest() (params idp.ListConfigurationsParams, api operations.ConsoleAPI) {
registerIDPHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Type = "ldap"
return params, api
}
func (suite *IDPTestSuite) TestListIDPConfigurationsWithoutError() {
ctx := context.Background()
res, err := listIDPConfigurations(ctx, "ldap", suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *IDPTestSuite) TestListIDPConfigurationsWithWrongType() {
ctx := context.Background()
res, err := listIDPConfigurations(ctx, "", suite.adminClient)
suite.assert.Nil(res)
suite.assert.NotNil(err)
}
func (suite *IDPTestSuite) TestDeleteIDPConfigurationHandlerWithError() {
params, api := suite.initDeleteIDPConfigurationRequest()
response := api.IdpDeleteConfigurationHandler.Handle(params, &models.Principal{})
_, ok := response.(*idp.DeleteConfigurationDefault)
suite.assert.True(ok)
}
func (suite *IDPTestSuite) initDeleteIDPConfigurationRequest() (params idp.DeleteConfigurationParams, api operations.ConsoleAPI) {
registerIDPHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Type = "ldap"
return params, api
}
func (suite *IDPTestSuite) TestDeleteIDPConfigurationWithoutError() {
ctx := context.Background()
_, err := deleteIDPConfig(ctx, "ldap", "", suite.adminClient)
suite.assert.Nil(err)
}
func (suite *IDPTestSuite) TestDeleteIDPConfigurationWithWrongType() {
ctx := context.Background()
_, err := deleteIDPConfig(ctx, "", "", suite.adminClient)
suite.assert.NotNil(err)
}
func (suite *IDPTestSuite) TestGetIDPConfigurationHandlerWithError() {
params, api := suite.initGetIDPConfigurationRequest()
response := api.IdpGetConfigurationHandler.Handle(params, &models.Principal{})
_, ok := response.(*idp.GetConfigurationDefault)
suite.assert.True(ok)
}
func (suite *IDPTestSuite) initGetIDPConfigurationRequest() (params idp.GetConfigurationParams, api operations.ConsoleAPI) {
registerIDPHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Type = "ldap"
return params, api
}
func (suite *IDPTestSuite) TestGetIDPConfigurationWithoutError() {
ctx := context.Background()
res, err := getIDPConfiguration(ctx, "ldap", "", suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *IDPTestSuite) TestGetIDPConfigurationWithWrongType() {
ctx := context.Background()
res, err := getIDPConfiguration(ctx, "", "", suite.adminClient)
suite.assert.Nil(res)
suite.assert.NotNil(err)
}
func TestIDP(t *testing.T) {
suite.Run(t, new(IDPTestSuite))
}
func TestGetEntitiesResult(t *testing.T) {
assert := assert.New(t)
// mock minIO client
client := AdminClientMock{}
function := "getEntitiesResult()"
usersList := []string{"user1", "user2", "user3"}
policiesList := []string{"policy1", "policy2", "policy3"}
groupsList := []string{"group1", "group3", "group5"}
policyMap := []madmin.PolicyEntities{
{Policy: "testPolicy0", Groups: groupsList, Users: usersList},
{Policy: "testPolicy1", Groups: groupsList, Users: usersList},
}
usersMap := []madmin.UserPolicyEntities{
{User: "testUser0", Policies: policiesList},
{User: "testUser1", Policies: policiesList},
}
groupsMap := []madmin.GroupPolicyEntities{
{Group: "group0", Policies: policiesList},
{Group: "group1", Policies: policiesList},
}
// Test-1: getEntitiesResult list all information provided
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mockResponse := madmin.PolicyEntitiesResult{
PolicyMappings: policyMap,
GroupMappings: groupsMap,
UserMappings: usersMap,
}
minioGetLDAPPolicyEntitiesMock = func(_ context.Context, _ madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
return mockResponse, nil
}
entities, err := getEntitiesResult(ctx, client, usersList, groupsList, policiesList)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
for i, groupIt := range entities.Groups {
assert.Equal(fmt.Sprintf("group%d", i), groupIt.Group)
for i, polItm := range groupIt.Policies {
assert.Equal(policiesList[i], polItm)
}
}
for i, usrIt := range entities.Users {
assert.Equal(fmt.Sprintf("testUser%d", i), usrIt.User)
for i, polItm := range usrIt.Policies {
assert.Equal(policiesList[i], polItm)
}
}
for i, policyIt := range entities.Policies {
assert.Equal(fmt.Sprintf("testPolicy%d", i), policyIt.Policy)
for i, userItm := range policyIt.Users {
assert.Equal(usersList[i], userItm)
}
for i, grItm := range policyIt.Groups {
assert.Equal(groupsList[i], grItm)
}
}
// Test-2: getEntitiesResult error is returned from getLDAPPolicyEntities()
minioGetLDAPPolicyEntitiesMock = func(_ context.Context, _ madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
return madmin.PolicyEntitiesResult{}, errors.New("error")
}
_, err = getEntitiesResult(ctx, client, usersList, groupsList, policiesList)
if assert.Error(err) {
assert.Equal("error", err.Error())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,152 +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"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/minio/console/pkg/utils"
"github.com/minio/console/api/operations"
systemApi "github.com/minio/console/api/operations/system"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type AdminInfoTestSuite struct {
suite.Suite
assert *assert.Assertions
currentServer string
isServerSet bool
isPrometheusRequest bool
server *httptest.Server
adminClient AdminClientMock
}
func (suite *AdminInfoTestSuite) SetupSuite() {
suite.assert = assert.New(suite.T())
suite.adminClient = AdminClientMock{}
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
return madmin.InfoMessage{
Servers: []madmin.ServerProperties{{
Disks: []madmin.Disk{{}},
}},
Backend: madmin.ErasureBackend{Type: "mock"},
}, nil
}
}
func (suite *AdminInfoTestSuite) SetupTest() {
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
os.Setenv(ConsoleMinIOServer, suite.server.URL)
}
func (suite *AdminInfoTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
if suite.isPrometheusRequest {
w.WriteHeader(200)
} else {
w.WriteHeader(400)
}
}
func (suite *AdminInfoTestSuite) TearDownSuite() {
}
func (suite *AdminInfoTestSuite) TearDownTest() {
if suite.isServerSet {
os.Setenv(ConsoleMinIOServer, suite.currentServer)
} else {
os.Unsetenv(ConsoleMinIOServer)
}
}
func (suite *AdminInfoTestSuite) TestRegisterAdminInfoHandlers() {
api := &operations.ConsoleAPI{}
suite.assertHandlersAreNil(api)
registerAdminInfoHandlers(api)
suite.assertHandlersAreNotNil(api)
}
func (suite *AdminInfoTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
suite.assert.Nil(api.SystemAdminInfoHandler)
suite.assert.Nil(api.SystemDashboardWidgetDetailsHandler)
}
func (suite *AdminInfoTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
suite.assert.NotNil(api.SystemAdminInfoHandler)
suite.assert.NotNil(api.SystemDashboardWidgetDetailsHandler)
}
func (suite *AdminInfoTestSuite) TestSystemAdminInfoHandlerWithError() {
params, api := suite.initSystemAdminInfoRequest()
response := api.SystemAdminInfoHandler.Handle(params, &models.Principal{})
_, ok := response.(*systemApi.AdminInfoDefault)
suite.assert.True(ok)
}
func (suite *AdminInfoTestSuite) initSystemAdminInfoRequest() (params systemApi.AdminInfoParams, api operations.ConsoleAPI) {
registerAdminInfoHandlers(&api)
params.HTTPRequest = &http.Request{}
defaultOnly := false
params.DefaultOnly = &defaultOnly
return params, api
}
func (suite *AdminInfoTestSuite) TestSystemDashboardWidgetDetailsHandlerWithError() {
params, api := suite.initSystemDashboardWidgetDetailsRequest()
response := api.SystemDashboardWidgetDetailsHandler.Handle(params, &models.Principal{})
_, ok := response.(*systemApi.DashboardWidgetDetailsDefault)
suite.assert.True(ok)
}
func (suite *AdminInfoTestSuite) initSystemDashboardWidgetDetailsRequest() (params systemApi.DashboardWidgetDetailsParams, api operations.ConsoleAPI) {
registerAdminInfoHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *AdminInfoTestSuite) TestGetUsageWidgetsForDeploymentWithoutError() {
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
suite.isPrometheusRequest = true
res, err := getUsageWidgetsForDeployment(ctx, suite.server.URL, suite.adminClient)
suite.assert.Nil(err)
suite.assert.NotNil(res)
suite.isPrometheusRequest = false
}
func (suite *AdminInfoTestSuite) TestGetWidgetDetailsWithoutError() {
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
suite.isPrometheusRequest = true
var step int32 = 1
var start int64
var end int64 = 1
res, err := getWidgetDetails(ctx, suite.server.URL, "mock", 1, &step, &start, &end)
suite.assert.Nil(err)
suite.assert.NotNil(res)
suite.isPrometheusRequest = false
}
func TestAdminInfo(t *testing.T) {
suite.Run(t, new(AdminInfoTestSuite))
}

View File

@@ -1,117 +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 (
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
inspectApi "github.com/minio/console/api/operations/inspect"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
"github.com/secure-io/sio-go"
)
func registerInspectHandler(api *operations.ConsoleAPI) {
api.InspectInspectHandler = inspectApi.InspectHandlerFunc(func(params inspectApi.InspectParams, principal *models.Principal) middleware.Responder {
k, r, err := getInspectResult(principal, &params)
if err != nil {
return inspectApi.NewInspectDefault(err.Code).WithPayload(err.APIError)
}
return middleware.ResponderFunc(processInspectResponse(&params, k, r))
})
}
func getInspectResult(session *models.Principal, params *inspectApi.InspectParams) ([]byte, io.ReadCloser, *CodedAPIError) {
ctx := params.HTTPRequest.Context()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return nil, nil, ErrorWithContext(ctx, err)
}
cfg := madmin.InspectOptions{
File: params.File,
Volume: params.Volume,
}
// TODO: Remove encryption option and always encrypt.
// Maybe also add public key field.
if params.Encrypt != nil && *params.Encrypt {
cfg.PublicKey, _ = base64.StdEncoding.DecodeString("MIIBCgKCAQEAs/128UFS9A8YSJY1XqYKt06dLVQQCGDee69T+0Tip/1jGAB4z0/3QMpH0MiS8Wjs4BRWV51qvkfAHzwwdU7y6jxU05ctb/H/WzRj3FYdhhHKdzear9TLJftlTs+xwj2XaADjbLXCV1jGLS889A7f7z5DgABlVZMQd9BjVAR8ED3xRJ2/ZCNuQVJ+A8r7TYPGMY3wWvhhPgPk3Lx4WDZxDiDNlFs4GQSaESSsiVTb9vyGe/94CsCTM6Cw9QG6ifHKCa/rFszPYdKCabAfHcS3eTr0GM+TThSsxO7KfuscbmLJkfQev1srfL2Ii2RbnysqIJVWKEwdW05ID8ryPkuTuwIDAQAB")
}
// create a MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
k, r, err := adminClient.inspect(ctx, cfg)
if err != nil {
return nil, nil, ErrorWithContext(ctx, err)
}
return k, r, nil
}
// borrowed from mc cli
func decryptInspectV1(key [32]byte, r io.Reader) io.ReadCloser {
stream, err := sio.AES_256_GCM.Stream(key[:])
if err != nil {
return nil
}
nonce := make([]byte, stream.NonceSize())
return io.NopCloser(stream.DecryptReader(r, nonce, nil))
}
func processInspectResponse(params *inspectApi.InspectParams, k []byte, r io.ReadCloser) func(w http.ResponseWriter, _ runtime.Producer) {
isEnc := params.Encrypt != nil && *params.Encrypt
return func(w http.ResponseWriter, _ runtime.Producer) {
ext := "enc"
if len(k) == 32 && !isEnc {
ext = "zip"
r = decryptInspectV1(*(*[32]byte)(k), r)
}
fileName := fmt.Sprintf("inspect-%s-%s.%s", params.Volume, params.File, ext)
fileName = strings.Map(func(r rune) rune {
switch {
case r >= 'A' && r <= 'Z':
return r
case r >= 'a' && r <= 'z':
return r
case r >= '0' && r <= '9':
return r
default:
if strings.ContainsAny(string(r), "-+._") {
return r
}
return '_'
}
}, fileName)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
_, err := io.Copy(w, r)
if err != nil {
LogError("unable to write all the data: %v", err)
}
}
}

View File

@@ -1,296 +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"
"sort"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
kmsAPI "github.com/minio/console/api/operations/k_m_s"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
)
func registerKMSHandlers(api *operations.ConsoleAPI) {
registerKMSStatusHandlers(api)
registerKMSKeyHandlers(api)
}
func registerKMSStatusHandlers(api *operations.ConsoleAPI) {
api.KmsKMSStatusHandler = kmsAPI.KMSStatusHandlerFunc(func(params kmsAPI.KMSStatusParams, session *models.Principal) middleware.Responder {
resp, err := GetKMSStatusResponse(session, params)
if err != nil {
return kmsAPI.NewKMSStatusDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSStatusOK().WithPayload(resp)
})
api.KmsKMSMetricsHandler = kmsAPI.KMSMetricsHandlerFunc(func(params kmsAPI.KMSMetricsParams, session *models.Principal) middleware.Responder {
resp, err := GetKMSMetricsResponse(session, params)
if err != nil {
return kmsAPI.NewKMSMetricsDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSMetricsOK().WithPayload(resp)
})
api.KmsKMSAPIsHandler = kmsAPI.KMSAPIsHandlerFunc(func(params kmsAPI.KMSAPIsParams, session *models.Principal) middleware.Responder {
resp, err := GetKMSAPIsResponse(session, params)
if err != nil {
return kmsAPI.NewKMSAPIsDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSAPIsOK().WithPayload(resp)
})
api.KmsKMSVersionHandler = kmsAPI.KMSVersionHandlerFunc(func(params kmsAPI.KMSVersionParams, session *models.Principal) middleware.Responder {
resp, err := GetKMSVersionResponse(session, params)
if err != nil {
return kmsAPI.NewKMSVersionDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSVersionOK().WithPayload(resp)
})
}
func GetKMSStatusResponse(session *models.Principal, params kmsAPI.KMSStatusParams) (*models.KmsStatusResponse, *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)
}
return kmsStatus(ctx, AdminClient{Client: mAdmin})
}
func kmsStatus(ctx context.Context, minioClient MinioAdmin) (*models.KmsStatusResponse, *CodedAPIError) {
st, err := minioClient.kmsStatus(ctx)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.KmsStatusResponse{
DefaultKeyID: st.DefaultKeyID,
Name: st.Name,
Endpoints: parseStatusEndpoints(st.Endpoints),
}, nil
}
func parseStatusEndpoints(endpoints map[string]madmin.ItemState) (kmsEndpoints []*models.KmsEndpoint) {
for key, value := range endpoints {
kmsEndpoints = append(kmsEndpoints, &models.KmsEndpoint{URL: key, Status: string(value)})
}
return kmsEndpoints
}
func GetKMSMetricsResponse(session *models.Principal, params kmsAPI.KMSMetricsParams) (*models.KmsMetricsResponse, *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)
}
return kmsMetrics(ctx, AdminClient{Client: mAdmin})
}
func kmsMetrics(ctx context.Context, minioClient MinioAdmin) (*models.KmsMetricsResponse, *CodedAPIError) {
metrics, err := minioClient.kmsMetrics(ctx)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.KmsMetricsResponse{
RequestOK: &metrics.RequestOK,
RequestErr: &metrics.RequestErr,
RequestFail: &metrics.RequestFail,
RequestActive: &metrics.RequestActive,
AuditEvents: &metrics.AuditEvents,
ErrorEvents: &metrics.ErrorEvents,
LatencyHistogram: parseHistogram(metrics.LatencyHistogram),
Uptime: &metrics.UpTime,
Cpus: &metrics.CPUs,
UsableCPUs: &metrics.UsableCPUs,
Threads: &metrics.Threads,
HeapAlloc: &metrics.HeapAlloc,
HeapObjects: metrics.HeapObjects,
StackAlloc: &metrics.StackAlloc,
}, nil
}
func parseHistogram(histogram map[int64]int64) (records []*models.KmsLatencyHistogram) {
for duration, total := range histogram {
records = append(records, &models.KmsLatencyHistogram{Duration: duration, Total: total})
}
cp := func(i, j int) bool {
return records[i].Duration < records[j].Duration
}
sort.Slice(records, cp)
return records
}
func GetKMSAPIsResponse(session *models.Principal, params kmsAPI.KMSAPIsParams) (*models.KmsAPIsResponse, *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)
}
return kmsAPIs(ctx, AdminClient{Client: mAdmin})
}
func kmsAPIs(ctx context.Context, minioClient MinioAdmin) (*models.KmsAPIsResponse, *CodedAPIError) {
apis, err := minioClient.kmsAPIs(ctx)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.KmsAPIsResponse{
Results: parseApis(apis),
}, nil
}
func parseApis(apis []madmin.KMSAPI) (data []*models.KmsAPI) {
for _, api := range apis {
data = append(data, &models.KmsAPI{
Method: api.Method,
Path: api.Path,
MaxBody: api.MaxBody,
Timeout: api.Timeout,
})
}
return data
}
func GetKMSVersionResponse(session *models.Principal, params kmsAPI.KMSVersionParams) (*models.KmsVersionResponse, *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)
}
return kmsVersion(ctx, AdminClient{Client: mAdmin})
}
func kmsVersion(ctx context.Context, minioClient MinioAdmin) (*models.KmsVersionResponse, *CodedAPIError) {
version, err := minioClient.kmsVersion(ctx)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.KmsVersionResponse{
Version: version.Version,
}, nil
}
func registerKMSKeyHandlers(api *operations.ConsoleAPI) {
api.KmsKMSCreateKeyHandler = kmsAPI.KMSCreateKeyHandlerFunc(func(params kmsAPI.KMSCreateKeyParams, session *models.Principal) middleware.Responder {
err := GetKMSCreateKeyResponse(session, params)
if err != nil {
return kmsAPI.NewKMSCreateKeyDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSCreateKeyCreated()
})
api.KmsKMSListKeysHandler = kmsAPI.KMSListKeysHandlerFunc(func(params kmsAPI.KMSListKeysParams, session *models.Principal) middleware.Responder {
resp, err := GetKMSListKeysResponse(session, params)
if err != nil {
return kmsAPI.NewKMSListKeysDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSListKeysOK().WithPayload(resp)
})
api.KmsKMSKeyStatusHandler = kmsAPI.KMSKeyStatusHandlerFunc(func(params kmsAPI.KMSKeyStatusParams, session *models.Principal) middleware.Responder {
resp, err := GetKMSKeyStatusResponse(session, params)
if err != nil {
return kmsAPI.NewKMSKeyStatusDefault(err.Code).WithPayload(err.APIError)
}
return kmsAPI.NewKMSKeyStatusOK().WithPayload(resp)
})
}
func GetKMSCreateKeyResponse(session *models.Principal, params kmsAPI.KMSCreateKeyParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
return createKey(ctx, *params.Body.Key, AdminClient{Client: mAdmin})
}
func createKey(ctx context.Context, key string, minioClient MinioAdmin) *CodedAPIError {
if err := minioClient.createKey(ctx, key); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func GetKMSListKeysResponse(session *models.Principal, params kmsAPI.KMSListKeysParams) (*models.KmsListKeysResponse, *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)
}
pattern := ""
if params.Pattern != nil {
pattern = *params.Pattern
}
return listKeys(ctx, pattern, AdminClient{Client: mAdmin})
}
func listKeys(ctx context.Context, pattern string, minioClient MinioAdmin) (*models.KmsListKeysResponse, *CodedAPIError) {
results, err := minioClient.listKeys(ctx, pattern)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.KmsListKeysResponse{Results: parseKeys(results)}, nil
}
// printDate - human friendly formatted date.
const (
printDate = "2006-01-02 15:04:05 MST"
)
func parseKeys(results []madmin.KMSKeyInfo) (data []*models.KmsKeyInfo) {
for _, key := range results {
data = append(data, &models.KmsKeyInfo{
CreatedAt: key.CreatedAt.Format(printDate),
CreatedBy: key.CreatedBy,
Name: key.Name,
})
}
return data
}
func GetKMSKeyStatusResponse(session *models.Principal, params kmsAPI.KMSKeyStatusParams) (*models.KmsKeyStatusResponse, *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)
}
return keyStatus(ctx, params.Name, AdminClient{Client: mAdmin})
}
func keyStatus(ctx context.Context, key string, minioClient MinioAdmin) (*models.KmsKeyStatusResponse, *CodedAPIError) {
ks, err := minioClient.keyStatus(ctx, key)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.KmsKeyStatusResponse{
KeyID: ks.KeyID,
EncryptionErr: ks.EncryptionErr,
DecryptionErr: ks.DecryptionErr,
}, nil
}

View File

@@ -1,238 +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"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/minio/console/api/operations"
kmsAPI "github.com/minio/console/api/operations/k_m_s"
"github.com/minio/console/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type KMSTestSuite struct {
suite.Suite
assert *assert.Assertions
currentServer string
isServerSet bool
server *httptest.Server
adminClient AdminClientMock
}
func (suite *KMSTestSuite) SetupSuite() {
suite.assert = assert.New(suite.T())
suite.adminClient = AdminClientMock{}
}
func (suite *KMSTestSuite) SetupTest() {
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
os.Setenv(ConsoleMinIOServer, suite.server.URL)
}
func (suite *KMSTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(400)
}
func (suite *KMSTestSuite) TearDownSuite() {
}
func (suite *KMSTestSuite) TearDownTest() {
if suite.isServerSet {
os.Setenv(ConsoleMinIOServer, suite.currentServer)
} else {
os.Unsetenv(ConsoleMinIOServer)
}
}
func (suite *KMSTestSuite) TestRegisterKMSHandlers() {
api := &operations.ConsoleAPI{}
suite.assertHandlersAreNil(api)
registerKMSHandlers(api)
suite.assertHandlersAreNotNil(api)
}
func (suite *KMSTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
suite.assert.Nil(api.KmsKMSStatusHandler)
suite.assert.Nil(api.KmsKMSMetricsHandler)
suite.assert.Nil(api.KmsKMSAPIsHandler)
suite.assert.Nil(api.KmsKMSVersionHandler)
suite.assert.Nil(api.KmsKMSCreateKeyHandler)
suite.assert.Nil(api.KmsKMSListKeysHandler)
suite.assert.Nil(api.KmsKMSKeyStatusHandler)
}
func (suite *KMSTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
suite.assert.NotNil(api.KmsKMSStatusHandler)
suite.assert.NotNil(api.KmsKMSMetricsHandler)
suite.assert.NotNil(api.KmsKMSAPIsHandler)
suite.assert.NotNil(api.KmsKMSVersionHandler)
suite.assert.NotNil(api.KmsKMSCreateKeyHandler)
suite.assert.NotNil(api.KmsKMSListKeysHandler)
suite.assert.NotNil(api.KmsKMSKeyStatusHandler)
}
func (suite *KMSTestSuite) TestKMSStatusHandlerWithError() {
params, api := suite.initKMSStatusRequest()
response := api.KmsKMSStatusHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSStatusDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSStatusRequest() (params kmsAPI.KMSStatusParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *KMSTestSuite) TestKMSStatusWithoutError() {
ctx := context.Background()
res, err := kmsStatus(ctx, suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *KMSTestSuite) TestKMSMetricsHandlerWithError() {
params, api := suite.initKMSMetricsRequest()
response := api.KmsKMSMetricsHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSMetricsDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSMetricsRequest() (params kmsAPI.KMSMetricsParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *KMSTestSuite) TestKMSMetricsWithoutError() {
ctx := context.Background()
res, err := kmsMetrics(ctx, suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *KMSTestSuite) TestKMSAPIsHandlerWithError() {
params, api := suite.initKMSAPIsRequest()
response := api.KmsKMSAPIsHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSAPIsDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSAPIsRequest() (params kmsAPI.KMSAPIsParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *KMSTestSuite) TestKMSAPIsWithoutError() {
ctx := context.Background()
res, err := kmsAPIs(ctx, suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *KMSTestSuite) TestKMSVersionHandlerWithError() {
params, api := suite.initKMSVersionRequest()
response := api.KmsKMSVersionHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSVersionDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSVersionRequest() (params kmsAPI.KMSVersionParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *KMSTestSuite) TestKMSVersionWithoutError() {
ctx := context.Background()
res, err := kmsVersion(ctx, suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *KMSTestSuite) TestKMSCreateKeyHandlerWithError() {
params, api := suite.initKMSCreateKeyRequest()
response := api.KmsKMSCreateKeyHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSCreateKeyDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSCreateKeyRequest() (params kmsAPI.KMSCreateKeyParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
key := "key"
params.Body = &models.KmsCreateKeyRequest{Key: &key}
return params, api
}
func (suite *KMSTestSuite) TestKMSCreateKeyWithoutError() {
ctx := context.Background()
err := createKey(ctx, "key", suite.adminClient)
suite.assert.Nil(err)
}
func (suite *KMSTestSuite) TestKMSListKeysHandlerWithError() {
params, api := suite.initKMSListKeysRequest()
response := api.KmsKMSListKeysHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSListKeysDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSListKeysRequest() (params kmsAPI.KMSListKeysParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *KMSTestSuite) TestKMSListKeysWithoutError() {
ctx := context.Background()
res, err := listKeys(ctx, "", suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *KMSTestSuite) TestKMSKeyStatusHandlerWithError() {
params, api := suite.initKMSKeyStatusRequest()
response := api.KmsKMSKeyStatusHandler.Handle(params, &models.Principal{})
_, ok := response.(*kmsAPI.KMSKeyStatusDefault)
suite.assert.True(ok)
}
func (suite *KMSTestSuite) initKMSKeyStatusRequest() (params kmsAPI.KMSKeyStatusParams, api operations.ConsoleAPI) {
registerKMSHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *KMSTestSuite) TestKMSKeyStatusWithoutError() {
ctx := context.Background()
res, err := keyStatus(ctx, "key", suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func TestKMS(t *testing.T) {
suite.Run(t, new(KMSTestSuite))
}

View File

@@ -1,55 +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"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
systemApi "github.com/minio/console/api/operations/system"
"github.com/minio/console/models"
)
func registerNodesHandler(api *operations.ConsoleAPI) {
api.SystemListNodesHandler = systemApi.ListNodesHandlerFunc(func(params systemApi.ListNodesParams, session *models.Principal) middleware.Responder {
listNodesResponse, err := getListNodesResponse(session, params)
if err != nil {
return systemApi.NewListNodesDefault(err.Code).WithPayload(err.APIError)
}
return systemApi.NewListNodesOK().WithPayload(listNodesResponse)
})
}
// getListNodesResponse returns a list of available node endpoints .
func getListNodesResponse(session *models.Principal, params systemApi.ListNodesParams) ([]string, *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)
}
var nodeList []string
adminResources, _ := mAdmin.ServerInfo(ctx)
for _, n := range adminResources.Servers {
nodeList = append(nodeList, n.Endpoint)
}
return nodeList, nil
}

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,698 +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"
"fmt"
"sort"
"strings"
bucketApi "github.com/minio/console/api/operations/bucket"
policyApi "github.com/minio/console/api/operations/policy"
s3 "github.com/minio/minio-go/v7"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
"github.com/minio/console/models"
iampolicy "github.com/minio/pkg/v3/policy"
policies "github.com/minio/console/api/policy"
)
func registersPoliciesHandler(api *operations.ConsoleAPI) {
// List Policies
api.PolicyListPoliciesHandler = policyApi.ListPoliciesHandlerFunc(func(params policyApi.ListPoliciesParams, session *models.Principal) middleware.Responder {
listPoliciesResponse, err := getListPoliciesResponse(session, params)
if err != nil {
return policyApi.NewListPoliciesDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewListPoliciesOK().WithPayload(listPoliciesResponse)
})
// Policy Info
api.PolicyPolicyInfoHandler = policyApi.PolicyInfoHandlerFunc(func(params policyApi.PolicyInfoParams, session *models.Principal) middleware.Responder {
policyInfo, err := getPolicyInfoResponse(session, params)
if err != nil {
return policyApi.NewPolicyInfoDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewPolicyInfoOK().WithPayload(policyInfo)
})
// Add Policy
api.PolicyAddPolicyHandler = policyApi.AddPolicyHandlerFunc(func(params policyApi.AddPolicyParams, session *models.Principal) middleware.Responder {
policyResponse, err := getAddPolicyResponse(session, params)
if err != nil {
return policyApi.NewAddPolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewAddPolicyCreated().WithPayload(policyResponse)
})
// Remove Policy
api.PolicyRemovePolicyHandler = policyApi.RemovePolicyHandlerFunc(func(params policyApi.RemovePolicyParams, session *models.Principal) middleware.Responder {
if err := getRemovePolicyResponse(session, params); err != nil {
return policyApi.NewRemovePolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewRemovePolicyNoContent()
})
// Set Policy
api.PolicySetPolicyHandler = policyApi.SetPolicyHandlerFunc(func(params policyApi.SetPolicyParams, session *models.Principal) middleware.Responder {
if err := getSetPolicyResponse(session, params); err != nil {
return policyApi.NewSetPolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewSetPolicyNoContent()
})
// Set Policy Multiple User/Groups
api.PolicySetPolicyMultipleHandler = policyApi.SetPolicyMultipleHandlerFunc(func(params policyApi.SetPolicyMultipleParams, session *models.Principal) middleware.Responder {
if err := getSetPolicyMultipleResponse(session, params); err != nil {
return policyApi.NewSetPolicyMultipleDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewSetPolicyMultipleNoContent()
})
api.BucketListPoliciesWithBucketHandler = bucketApi.ListPoliciesWithBucketHandlerFunc(func(params bucketApi.ListPoliciesWithBucketParams, session *models.Principal) middleware.Responder {
policyResponse, err := getListPoliciesWithBucketResponse(session, params)
if err != nil {
return bucketApi.NewListPoliciesWithBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewListPoliciesWithBucketOK().WithPayload(policyResponse)
})
api.BucketListAccessRulesWithBucketHandler = bucketApi.ListAccessRulesWithBucketHandlerFunc(func(params bucketApi.ListAccessRulesWithBucketParams, session *models.Principal) middleware.Responder {
policyResponse, err := getListAccessRulesWithBucketResponse(session, params)
if err != nil {
return bucketApi.NewListAccessRulesWithBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewListAccessRulesWithBucketOK().WithPayload(policyResponse)
})
api.BucketSetAccessRuleWithBucketHandler = bucketApi.SetAccessRuleWithBucketHandlerFunc(func(params bucketApi.SetAccessRuleWithBucketParams, session *models.Principal) middleware.Responder {
policyResponse, err := getSetAccessRuleWithBucketResponse(session, params)
if err != nil {
return bucketApi.NewSetAccessRuleWithBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewSetAccessRuleWithBucketOK().WithPayload(policyResponse)
})
api.BucketDeleteAccessRuleWithBucketHandler = bucketApi.DeleteAccessRuleWithBucketHandlerFunc(func(params bucketApi.DeleteAccessRuleWithBucketParams, session *models.Principal) middleware.Responder {
policyResponse, err := getDeleteAccessRuleWithBucketResponse(session, params)
if err != nil {
return bucketApi.NewDeleteAccessRuleWithBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewDeleteAccessRuleWithBucketOK().WithPayload(policyResponse)
})
api.PolicyListUsersForPolicyHandler = policyApi.ListUsersForPolicyHandlerFunc(func(params policyApi.ListUsersForPolicyParams, session *models.Principal) middleware.Responder {
policyUsersResponse, err := getListUsersForPolicyResponse(session, params)
if err != nil {
return policyApi.NewListUsersForPolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewListUsersForPolicyOK().WithPayload(policyUsersResponse)
})
api.PolicyListGroupsForPolicyHandler = policyApi.ListGroupsForPolicyHandlerFunc(func(params policyApi.ListGroupsForPolicyParams, session *models.Principal) middleware.Responder {
policyGroupsResponse, err := getListGroupsForPolicyResponse(session, params)
if err != nil {
return policyApi.NewListGroupsForPolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewListGroupsForPolicyOK().WithPayload(policyGroupsResponse)
})
// Gets policies for currently logged in user
api.PolicyGetUserPolicyHandler = policyApi.GetUserPolicyHandlerFunc(func(params policyApi.GetUserPolicyParams, session *models.Principal) middleware.Responder {
userPolicyResponse, err := getUserPolicyResponse(params.HTTPRequest.Context(), session)
if err != nil {
return policyApi.NewGetUserPolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewGetUserPolicyOK().WithPayload(userPolicyResponse)
})
// Gets policies for specified user
api.PolicyGetSAUserPolicyHandler = policyApi.GetSAUserPolicyHandlerFunc(func(params policyApi.GetSAUserPolicyParams, session *models.Principal) middleware.Responder {
userPolicyResponse, err := getSAUserPolicyResponse(session, params)
if err != nil {
return policyApi.NewGetSAUserPolicyDefault(err.Code).WithPayload(err.APIError)
}
return policyApi.NewGetSAUserPolicyOK().WithPayload(userPolicyResponse)
})
}
func getListAccessRulesWithBucketResponse(session *models.Principal, params bucketApi.ListAccessRulesWithBucketParams) (*models.ListAccessRulesResponse, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
bucket := params.Bucket
client, err := newS3BucketClient(session, bucket, "", getClientIP(params.HTTPRequest))
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
accessRules, _ := client.GetAccessRules(ctx)
var accessRuleList []*models.AccessRule
for k, v := range accessRules {
accessRuleList = append(accessRuleList, &models.AccessRule{Prefix: k[len(bucket)+1 : len(k)-1], Access: v})
}
return &models.ListAccessRulesResponse{AccessRules: accessRuleList}, nil
}
func getSetAccessRuleWithBucketResponse(session *models.Principal, params bucketApi.SetAccessRuleWithBucketParams) (bool, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
prefixAccess := params.Prefixaccess
client, err := newS3BucketClient(session, params.Bucket, prefixAccess.Prefix, getClientIP(params.HTTPRequest))
if err != nil {
return false, ErrorWithContext(ctx, err)
}
errorVal := client.SetAccess(ctx, prefixAccess.Access, false)
if errorVal != nil {
returnError := ErrorWithContext(ctx, errorVal.Cause)
minioError := s3.ToErrorResponse(errorVal.Cause)
if minioError.Code == "NoSuchBucket" {
returnError.Code = 404
}
return false, returnError
}
return true, nil
}
func getDeleteAccessRuleWithBucketResponse(session *models.Principal, params bucketApi.DeleteAccessRuleWithBucketParams) (bool, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
bucket := params.Bucket
prefix := params.Prefix
client, err := newS3BucketClient(session, bucket, prefix.Prefix, getClientIP(params.HTTPRequest))
if err != nil {
return false, ErrorWithContext(ctx, err)
}
errorVal := client.SetAccess(ctx, "none", false)
if errorVal != nil {
return false, ErrorWithContext(ctx, errorVal.Cause)
}
return true, nil
}
func getListPoliciesWithBucketResponse(session *models.Principal, params bucketApi.ListPoliciesWithBucketParams) (*models.ListPoliciesResponse, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
policies, err := listPoliciesWithBucket(ctx, params.Bucket, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// serialize output
listPoliciesResponse := &models.ListPoliciesResponse{
Policies: policies,
Total: int64(len(policies)),
}
return listPoliciesResponse, nil
}
// listPoliciesWithBucket calls MinIO server to list all policy names present on the server that apply to a particular bucket.
// listPoliciesWithBucket() converts the map[string][]byte returned by client.listPolicies()
// to []*models.Policy by iterating over each key in policyRawMap and
// then using Unmarshal on the raw bytes to create a *models.Policy
func listPoliciesWithBucket(ctx context.Context, bucket string, client MinioAdmin) ([]*models.Policy, error) {
policyMap, err := client.listPolicies(ctx)
var policies []*models.Policy
if err != nil {
return nil, err
}
for name, policy := range policyMap {
policy, err := parsePolicy(name, policy)
if err != nil {
return nil, err
}
if policyMatchesBucket(ctx, policy, bucket) {
policies = append(policies, policy)
}
}
return policies, nil
}
func policyMatchesBucket(ctx context.Context, policy *models.Policy, bucket string) bool {
policyData := &iampolicy.Policy{}
err := json.Unmarshal([]byte(policy.Policy), policyData)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error parsing policy: %v", err))
return false
}
policyStatements := policyData.Statements
for i := 0; i < len(policyStatements); i++ {
resources := policyStatements[i].Resources
if resources.Match(bucket, map[string][]string{}) {
return true
}
if resources.Match(fmt.Sprintf("%s/*", bucket), map[string][]string{}) {
return true
}
}
return false
}
// listPolicies calls MinIO server to list all policy names present on the server.
// listPolicies() converts the map[string][]byte returned by client.listPolicies()
// to []*models.Policy by iterating over each key in policyRawMap and
// then using Unmarshal on the raw bytes to create a *models.Policy
func listPolicies(ctx context.Context, client MinioAdmin) ([]*models.Policy, error) {
policyMap, err := client.listPolicies(ctx)
var policies []*models.Policy
if err != nil {
return nil, err
}
for name, policy := range policyMap {
policy, err := parsePolicy(name, policy)
if err != nil {
return nil, err
}
policies = append(policies, policy)
}
return policies, nil
}
// getListPoliciesResponse performs listPolicies() and serializes it to the handler's output
func getListPoliciesResponse(session *models.Principal, params policyApi.ListPoliciesParams) (*models.ListPoliciesResponse, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
policies, err := listPolicies(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// serialize output
listPoliciesResponse := &models.ListPoliciesResponse{
Policies: policies,
Total: int64(len(policies)),
}
return listPoliciesResponse, nil
}
// getListUsersForPoliciesResponse performs lists users affected by a given policy.
func getListUsersForPolicyResponse(session *models.Principal, params policyApi.ListUsersForPolicyParams) ([]string, *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}
policies, err := listPolicies(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
found := false
for i := range policies {
if policies[i].Name == params.Policy {
found = true
}
}
if !found {
return nil, ErrorWithContext(ctx, ErrPolicyNotFound, fmt.Errorf("the policy %s does not exist", params.Policy))
}
users, err := listUsers(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
var filteredUsers []string
for _, user := range users {
for _, upolicy := range user.Policy {
if upolicy == params.Policy {
filteredUsers = append(filteredUsers, user.AccessKey)
break
}
}
}
sort.Strings(filteredUsers)
return filteredUsers, nil
}
func getUserPolicyResponse(ctx context.Context, session *models.Principal) (string, *CodedAPIError) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// serialize output
if session == nil {
return "nil", ErrorWithContext(ctx, ErrPolicyNotFound)
}
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)
}
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)
}
rawPolicy := policies.ReplacePolicyVariables(tokenClaims, accountInfo)
return string(rawPolicy), nil
}
func getSAUserPolicyResponse(session *models.Principal, params policyApi.GetSAUserPolicyParams) (*models.AUserPolicyResponse, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// serialize output
if session == nil {
return nil, ErrorWithContext(ctx, ErrPolicyNotFound)
}
// initialize admin client
mAdminClient, err := NewMinioAdminClient(params.HTTPRequest.Context(), &models.Principal{
STSAccessKeyID: session.STSAccessKeyID,
STSSecretAccessKey: session.STSSecretAccessKey,
STSSessionToken: session.STSSessionToken,
})
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
userAdminClient := AdminClient{Client: mAdminClient}
user, err := getUserInfo(ctx, userAdminClient, params.Name)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
var userPolicies []string
if len(user.PolicyName) > 0 {
userPolicies = strings.Split(user.PolicyName, ",")
}
for _, group := range user.MemberOf {
groupDesc, err := groupInfo(ctx, userAdminClient, group)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
if groupDesc.Policy != "" {
userPolicies = append(userPolicies, strings.Split(groupDesc.Policy, ",")...)
}
}
allKeys := make(map[string]bool)
var userPolicyList []string
for _, item := range userPolicies {
if _, value := allKeys[item]; !value {
allKeys[item] = true
userPolicyList = append(userPolicyList, item)
}
}
var userStatements []iampolicy.Statement
for _, pol := range userPolicyList {
policy, err := getPolicyStatements(ctx, userAdminClient, pol)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
userStatements = append(userStatements, policy...)
}
combinedPolicy := iampolicy.Policy{
Version: "2012-10-17",
Statements: userStatements,
}
stringPolicy, err := json.Marshal(combinedPolicy)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
parsedPolicy := string(stringPolicy)
getUserPoliciesResponse := &models.AUserPolicyResponse{
Policy: parsedPolicy,
}
return getUserPoliciesResponse, nil
}
func getListGroupsForPolicyResponse(session *models.Principal, params policyApi.ListGroupsForPolicyParams) ([]string, *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}
policies, err := listPolicies(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
found := false
for i := range policies {
if policies[i].Name == params.Policy {
found = true
}
}
if !found {
return nil, ErrorWithContext(ctx, ErrPolicyNotFound, fmt.Errorf("the policy %s does not exist", params.Policy))
}
groups, err := adminClient.listGroups(ctx)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
var filteredGroups []string
for _, group := range groups {
info, err := groupInfo(ctx, adminClient, group)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
groupPolicies := strings.Split(info.Policy, ",")
for _, groupPolicy := range groupPolicies {
if groupPolicy == params.Policy {
filteredGroups = append(filteredGroups, group)
}
}
}
sort.Strings(filteredGroups)
return filteredGroups, nil
}
// removePolicy() calls MinIO server to remove a policy based on name.
func removePolicy(ctx context.Context, client MinioAdmin, name string) error {
err := client.removePolicy(ctx, name)
if err != nil {
return err
}
return nil
}
// getRemovePolicyResponse() performs removePolicy() and serializes it to the handler's output
func getRemovePolicyResponse(session *models.Principal, params policyApi.RemovePolicyParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
if params.Name == "" {
return ErrorWithContext(ctx, ErrPolicyNameNotInRequest)
}
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
if err := removePolicy(ctx, adminClient, params.Name); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
// addPolicy calls MinIO server to add a canned policy.
// addPolicy() takes name and policy in string format, policy
// policy must be string in json format, in the future this will change
// to a Policy struct{} - https://github.com/minio/minio/issues/9171
func addPolicy(ctx context.Context, client MinioAdmin, name, policy string) (*models.Policy, error) {
iamp, err := iampolicy.ParseConfig(bytes.NewReader([]byte(policy)))
if err != nil {
return nil, err
}
if err := client.addPolicy(ctx, name, iamp); err != nil {
return nil, err
}
policyObject, err := policyInfo(ctx, client, name)
if err != nil {
return nil, err
}
return policyObject, nil
}
// getAddPolicyResponse performs addPolicy() and serializes it to the handler's output
func getAddPolicyResponse(session *models.Principal, params policyApi.AddPolicyParams) (*models.Policy, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
if params.Body == nil {
return nil, ErrorWithContext(ctx, ErrPolicyBodyNotInRequest)
}
if strings.Contains(*params.Body.Name, " ") {
return nil, ErrorWithContext(ctx, ErrPolicyNameContainsSpace)
}
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// create a MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
policy, err := addPolicy(ctx, adminClient, *params.Body.Name, *params.Body.Policy)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return policy, nil
}
// policyInfo calls MinIO server to retrieve information of a canned policy.
// policyInfo() takes a policy name, obtains the []byte (represents a string in JSON format)
// and return it as *models.Policy , in the future this will change
// to a Policy struct{} - https://github.com/minio/minio/issues/9171
func policyInfo(ctx context.Context, client MinioAdmin, name string) (*models.Policy, error) {
policyRaw, err := client.getPolicy(ctx, name)
if err != nil {
return nil, err
}
policy, err := parsePolicy(name, policyRaw)
if err != nil {
return nil, err
}
return policy, nil
}
// getPolicy Statements calls MinIO server to retrieve information of a canned policy.
// and returns the associated Statements
func getPolicyStatements(ctx context.Context, client MinioAdmin, name string) ([]iampolicy.Statement, error) {
policyRaw, err := client.getPolicy(ctx, name)
if err != nil {
return nil, err
}
return policyRaw.Statements, nil
}
// getPolicyInfoResponse performs policyInfo() and serializes it to the handler's output
func getPolicyInfoResponse(session *models.Principal, params policyApi.PolicyInfoParams) (*models.Policy, *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 MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
policy, err := policyInfo(ctx, adminClient, params.Name)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return policy, nil
}
// SetPolicy calls MinIO server to assign policy to a group or user.
func SetPolicy(ctx context.Context, client MinioAdmin, name, entityName string, entityType models.PolicyEntity) error {
isGroup := false
if entityType == models.PolicyEntityGroup {
isGroup = true
}
return client.setPolicy(ctx, name, entityName, isGroup)
}
// getSetPolicyResponse() performs SetPolicy() and serializes it to the handler's output
func getSetPolicyResponse(session *models.Principal, params policyApi.SetPolicyParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// Removing this section
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
if err := SetPolicy(ctx, adminClient, strings.Join(params.Body.Name, ","), *params.Body.EntityName, *params.Body.EntityType); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func getSetPolicyMultipleResponse(session *models.Principal, params policyApi.SetPolicyMultipleParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
if err := setPolicyMultipleEntities(ctx, adminClient, strings.Join(params.Body.Name, ","), params.Body.Users, params.Body.Groups); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
// setPolicyMultipleEntities sets a policy to multiple users/groups
func setPolicyMultipleEntities(ctx context.Context, client MinioAdmin, policyName string, users, groups []models.IamEntity) error {
for _, user := range users {
if err := client.setPolicy(ctx, policyName, string(user), false); err != nil {
return err
}
}
for _, group := range groups {
groupDesc, err := groupInfo(ctx, client, string(group))
if err != nil {
return err
}
allGroupPolicies := ""
if len(groups) > 1 {
allGroupPolicies = groupDesc.Policy + "," + policyName
s := strings.Split(allGroupPolicies, ",")
allGroupPolicies = strings.Join(UniqueKeys(s), ",")
} else {
allGroupPolicies = policyName
}
if err := client.setPolicy(ctx, allGroupPolicies, string(group), true); err != nil {
return err
}
}
return nil
}
// parsePolicy() converts from *rawPolicy to *models.Policy
func parsePolicy(name string, rawPolicy *iampolicy.Policy) (*models.Policy, error) {
stringPolicy, err := json.Marshal(rawPolicy)
if err != nil {
return nil, err
}
policy := &models.Policy{
Name: name,
Policy: string(stringPolicy),
}
return policy, nil
}

View File

@@ -1,382 +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"
"errors"
"fmt"
"reflect"
"testing"
"github.com/minio/console/models"
iampolicy "github.com/minio/pkg/v3/policy"
"github.com/stretchr/testify/assert"
)
func TestListPolicies(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
funcAssert := assert.New(t)
adminClient := AdminClientMock{}
// mock function response from listPolicies()
minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
var readonly iampolicy.Policy
var readwrite iampolicy.Policy
var diagnostis iampolicy.Policy
for _, p := range iampolicy.DefaultPolicies {
switch p.Name {
case "readonly":
readonly = p.Definition
case "readwrite":
readwrite = p.Definition
case "diagnostics":
diagnostis = p.Definition
}
}
return map[string]*iampolicy.Policy{
"readonly": &readonly,
"readwrite": &readwrite,
"diagnostics": &diagnostis,
}, nil
}
// Test-1 : listPolicies() Get response from minio client with three Canned Policies and return the same number on listPolicies()
function := "listPolicies()"
policiesList, err := listPolicies(ctx, adminClient)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// verify length of Policies is correct
funcAssert.Equal(3, len(policiesList), fmt.Sprintf("Failed on %s: length of Policies's lists is not the same", function))
// Test-2 : listPolicies() Return error and see that the error is handled correctly and returned
minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
return nil, errors.New("error")
}
_, err = listPolicies(ctx, adminClient)
if funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
}
}
func TestRemovePolicy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
funcAssert := assert.New(t)
adminClient := AdminClientMock{}
// Test-1 : removePolicy() remove an existing policy
policyToRemove := "console-policy"
minioRemovePolicyMock = func(_ string) error {
return nil
}
function := "removePolicy()"
if err := removePolicy(ctx, adminClient, policyToRemove); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-2 : removePolicy() Return error and see that the error is handled correctly and returned
minioRemovePolicyMock = func(_ string) error {
return errors.New("error")
}
if err := removePolicy(ctx, adminClient, policyToRemove); funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
}
}
func TestAddPolicy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
funcAssert := assert.New(t)
adminClient := AdminClientMock{}
policyName := "new-policy"
policyDefinition := "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}"
minioAddPolicyMock = func(_ string, _ *iampolicy.Policy) error {
return nil
}
minioGetPolicyMock = func(_ string) (*iampolicy.Policy, error) {
policy := "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}"
iamp, err := iampolicy.ParseConfig(bytes.NewReader([]byte(policy)))
if err != nil {
return nil, err
}
return iamp, nil
}
assertPolicy := models.Policy{
Name: "new-policy",
Policy: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}",
}
// Test-1 : addPolicy() adds a new policy
function := "addPolicy()"
policy, err := addPolicy(ctx, adminClient, policyName, policyDefinition)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} else {
funcAssert.Equal(policy.Name, assertPolicy.Name)
var expectedPolicy iampolicy.Policy
var actualPolicy iampolicy.Policy
err1 := json.Unmarshal([]byte(policy.Policy), &expectedPolicy)
funcAssert.NoError(err1)
err2 := json.Unmarshal([]byte(assertPolicy.Policy), &actualPolicy)
funcAssert.NoError(err2)
funcAssert.Equal(expectedPolicy, actualPolicy)
}
// Test-2 : addPolicy() got an error while adding policy
minioAddPolicyMock = func(_ string, _ *iampolicy.Policy) error {
return errors.New("error")
}
if _, err := addPolicy(ctx, adminClient, policyName, policyDefinition); funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
}
// Test-3 : addPolicy() got an error while retrieving policy
minioAddPolicyMock = func(_ string, _ *iampolicy.Policy) error {
return nil
}
minioGetPolicyMock = func(_ string) (*iampolicy.Policy, error) {
return nil, errors.New("error")
}
if _, err := addPolicy(ctx, adminClient, policyName, policyDefinition); funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
}
}
func TestSetPolicy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
funcAssert := assert.New(t)
adminClient := AdminClientMock{}
policyName := "readOnly"
entityName := "alevsk"
entityObject := models.PolicyEntityUser
minioSetPolicyMock = func(_, _ string, _ bool) error {
return nil
}
// Test-1 : SetPolicy() set policy to user
function := "SetPolicy()"
err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-2 : SetPolicy() set policy to group
entityObject = models.PolicyEntityGroup
err = SetPolicy(ctx, adminClient, policyName, entityName, entityObject)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-3 : SetPolicy() set policy to user and get error
entityObject = models.PolicyEntityUser
minioSetPolicyMock = func(_, _ string, _ bool) error {
return errors.New("error")
}
if err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject); funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
}
// Test-4 : SetPolicy() set policy to group and get error
entityObject = models.PolicyEntityGroup
minioSetPolicyMock = func(_, _ string, _ bool) error {
return errors.New("error")
}
if err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject); funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
}
}
func Test_SetPolicyMultiple(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
adminClient := AdminClientMock{}
type args struct {
policyName string
users []models.IamEntity
groups []models.IamEntity
setPolicyFunc func(policyName, entityName string, isGroup bool) error
}
tests := []struct {
name string
args args
errorExpected error
}{
{
name: "Set policy to multiple users and groups",
args: args{
policyName: "readonly",
users: []models.IamEntity{"user1", "user2"},
groups: []models.IamEntity{"group1", "group2"},
setPolicyFunc: func(_, _ string, _ bool) error {
return nil
},
},
errorExpected: nil,
},
{
name: "Return error on set policy function",
args: args{
policyName: "readonly",
users: []models.IamEntity{"user1", "user2"},
groups: []models.IamEntity{"group1", "group2"},
setPolicyFunc: func(_, _ string, _ bool) error {
return errors.New("error set")
},
},
errorExpected: errors.New("error set"),
},
{
// Description: Empty lists of users and groups are acceptable
name: "Empty lists of users and groups",
args: args{
policyName: "readonly",
users: []models.IamEntity{},
groups: []models.IamEntity{},
setPolicyFunc: func(_, _ string, _ bool) error {
return nil
},
},
errorExpected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(_ *testing.T) {
minioSetPolicyMock = tt.args.setPolicyFunc
got := setPolicyMultipleEntities(ctx, adminClient, tt.args.policyName, tt.args.users, tt.args.groups)
if !reflect.DeepEqual(got, tt.errorExpected) {
ji, _ := json.Marshal(got)
vi, _ := json.Marshal(tt.errorExpected)
t.Errorf("got %s want %s", ji, vi)
}
})
}
}
func Test_policyMatchesBucket(t *testing.T) {
type args struct {
ctx context.Context
policy *models.Policy
bucket string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Test1",
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"admin:*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`}, bucket: "test1"},
want: true,
},
{
name: "Test2",
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::bucket1"
]
}
]
}`}, bucket: "test1"},
want: false,
},
{
name: "Test3",
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:ListStorageLensConfigurations",
"s3:GetAccessPoint",
"s3:PutAccountPublicAccessBlock",
"s3:GetAccountPublicAccessBlock",
"s3:ListAllMyBuckets",
"s3:ListAccessPoints",
"s3:ListJobs",
"s3:PutStorageLensConfiguration",
"s3:CreateJob"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::test",
"arn:aws:s3:::test/*",
"arn:aws:s3:::lkasdkljasd090901",
"arn:aws:s3:::lkasdkljasd090901/*"
]
}
]
}`}, bucket: "test1"},
want: false,
},
{
name: "Test4",
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::bucket1"
]
}
]
}`}, bucket: "bucket1"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(_ *testing.T) {
if got := policyMatchesBucket(tt.args.ctx, tt.args.policy, tt.args.bucket); got != tt.want {
t.Errorf("policyMatchesBucket() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,63 +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"
"io"
"net/http"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
"github.com/minio/websocket"
)
var items []*models.StartProfilingItem
type profileOptions struct {
Types string
}
func getProfileOptionsFromReq(req *http.Request) (*profileOptions, error) {
pOptions := profileOptions{}
pOptions.Types = req.FormValue("types")
return &pOptions, nil
}
func startProfiling(ctx context.Context, conn WSConn, client MinioAdmin, pOpts *profileOptions) error {
profilingResults, err := client.startProfiling(ctx, madmin.ProfilerType(pOpts.Types))
if err != nil {
return err
}
items = []*models.StartProfilingItem{}
for _, result := range profilingResults {
items = append(items, &models.StartProfilingItem{
Success: result.Success,
Error: result.Error,
NodeName: result.NodeName,
})
}
zippedData, err := client.stopProfiling(ctx)
if err != nil {
return err
}
message, err := io.ReadAll(zippedData)
if err != nil {
return err
}
return conn.writeMessage(websocket.BinaryMessage, message)
}

View File

@@ -1,105 +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"
"errors"
"io"
"net/http"
"net/url"
"testing"
"github.com/minio/madmin-go/v3"
"github.com/stretchr/testify/assert"
)
// Implementing fake closingBuffer to mock stopProfiling() (io.ReadCloser, error)
type ClosingBuffer struct {
*bytes.Buffer
}
// Implementing a fake Close function for io.ReadCloser
func (cb *ClosingBuffer) Close() error {
return nil
}
func TestStartProfiling(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
assert := assert.New(t)
adminClient := AdminClientMock{}
mockWSConn := mockConn{}
function := "startProfiling()"
testOptions := &profileOptions{
Types: "cpu",
}
// Test-1 : startProfiling() Get response from MinIO server with one profiling object without errors
// mock function response from startProfiling()
minioStartProfiling = func(_ madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
return []madmin.StartProfilingResult{
{
NodeName: "http://127.0.0.1:9000/",
Success: true,
Error: "",
},
{
NodeName: "http://127.0.0.1:9001/",
Success: true,
Error: "",
},
}, nil
}
// mock function response from stopProfiling()
minioStopProfiling = func() (io.ReadCloser, error) {
return &ClosingBuffer{bytes.NewBufferString("In memory string eaeae")}, nil
}
// mock function response from mockConn.writeMessage()
connWriteMessageMock = func(_ int, _ []byte) error {
return nil
}
err := startProfiling(ctx, mockWSConn, adminClient, testOptions)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
assert.Equal(err, nil)
// Test-2 : startProfiling() Correctly handles errors returned by MinIO
// mock function response from startProfiling()
minioStartProfiling = func(_ madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
return nil, errors.New("error")
}
err = startProfiling(ctx, mockWSConn, adminClient, testOptions)
if assert.Error(err) {
assert.Equal("error", err.Error())
}
// Test-3: getProfileOptionsFromReq() correctly returns profile options from request
u, _ := url.Parse("ws://localhost/ws/profile?types=cpu,mem,block,mutex,trace,threads,goroutines")
req := &http.Request{
URL: u,
}
opts, err := getProfileOptionsFromReq(req)
if assert.NoError(err) {
expectedOptions := profileOptions{
Types: "cpu,mem,block,mutex,trace,threads,goroutines",
}
assert.Equal(expectedOptions.Types, opts.Types)
}
}

View File

@@ -1,116 +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"
"net/http"
"net/url"
"time"
"github.com/minio/console/pkg/utils"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
release "github.com/minio/console/api/operations/release"
"github.com/minio/console/models"
"github.com/minio/pkg/v3/env"
)
var (
releaseServiceHostEnvVar = "RELEASE_SERVICE_HOST"
defaultReleaseServiceHost = "https://enterprise-updates.ic.min.dev"
)
func registerReleasesHandlers(api *operations.ConsoleAPI) {
api.ReleaseListReleasesHandler = release.ListReleasesHandlerFunc(func(params release.ListReleasesParams, session *models.Principal) middleware.Responder {
resp, err := GetReleaseListResponse(session, params)
if err != nil {
return release.NewListReleasesDefault(err.Code).WithPayload(err.APIError)
}
return release.NewListReleasesOK().WithPayload(resp)
})
}
func GetReleaseListResponse(_ *models.Principal, params release.ListReleasesParams) (*models.ReleaseListResponse, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
repo := params.Repo
currentRelease := ""
if params.Current != nil {
currentRelease = *params.Current
}
search := ""
if params.Search != nil {
search = *params.Search
}
filter := ""
if params.Filter != nil {
filter = *params.Filter
}
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
return releaseList(ctx, repo, currentRelease, search, filter)
}
func releaseList(ctx context.Context, repo, currentRelease, search, filter string) (*models.ReleaseListResponse, *CodedAPIError) {
serviceURL := getReleaseServiceURL()
clientIP := utils.ClientIPFromContext(ctx)
releases, err := getReleases(serviceURL, repo, currentRelease, search, filter, clientIP)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return releases, nil
}
func getReleaseServiceURL() string {
host := env.Get(releaseServiceHostEnvVar, defaultReleaseServiceHost)
return fmt.Sprintf("%s/releases", host)
}
func getReleases(endpoint, repo, currentRelease, search, filter, clientIP string) (*models.ReleaseListResponse, error) {
rl := &models.ReleaseListResponse{}
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
q := &url.Values{}
q.Add("repo", repo)
q.Add("search", search)
q.Add("filter", filter)
q.Add("current", currentRelease)
req.URL.RawQuery = q.Encode()
req.Header.Set("Content-Type", "application/json")
client := GetConsoleHTTPClient(clientIP)
client.Timeout = time.Second * 5
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error getting releases: %s", resp.Status)
}
err = json.NewDecoder(resp.Body).Decode(&rl)
if err != nil {
return nil, err
}
return rl, nil
}

View File

@@ -1,104 +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 (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/minio/console/api/operations"
release "github.com/minio/console/api/operations/release"
"github.com/minio/console/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ReleasesTestSuite struct {
suite.Suite
assert *assert.Assertions
currentServer string
isServerSet bool
getServer *httptest.Server
withError bool
}
func (suite *ReleasesTestSuite) SetupSuite() {
suite.assert = assert.New(suite.T())
suite.getServer = httptest.NewServer(http.HandlerFunc(suite.getHandler))
suite.currentServer, suite.isServerSet = os.LookupEnv(releaseServiceHostEnvVar)
os.Setenv(releaseServiceHostEnvVar, suite.getServer.URL)
}
func (suite *ReleasesTestSuite) TearDownSuite() {
if suite.isServerSet {
os.Setenv(releaseServiceHostEnvVar, suite.currentServer)
} else {
os.Unsetenv(releaseServiceHostEnvVar)
}
}
func (suite *ReleasesTestSuite) getHandler(
w http.ResponseWriter, _ *http.Request,
) {
if suite.withError {
w.WriteHeader(400)
} else {
w.WriteHeader(200)
response := &models.ReleaseListResponse{}
bytes, _ := json.Marshal(response)
fmt.Fprint(w, string(bytes))
}
}
func (suite *ReleasesTestSuite) TestRegisterReleasesHandlers() {
api := &operations.ConsoleAPI{}
suite.assert.Nil(api.ReleaseListReleasesHandler)
registerReleasesHandlers(api)
suite.assert.NotNil(api.ReleaseListReleasesHandler)
}
func (suite *ReleasesTestSuite) TestGetReleasesWithError() {
api := &operations.ConsoleAPI{}
current := "mock"
registerReleasesHandlers(api)
params := release.NewListReleasesParams()
params.Current = &current
params.HTTPRequest = &http.Request{}
suite.withError = true
response := api.ReleaseListReleasesHandler.Handle(params, &models.Principal{})
_, ok := response.(*release.ListReleasesDefault)
suite.assert.True(ok)
}
func (suite *ReleasesTestSuite) TestGetReleasesWithoutError() {
api := &operations.ConsoleAPI{}
registerReleasesHandlers(api)
params := release.NewListReleasesParams()
params.HTTPRequest = &http.Request{}
suite.withError = false
response := api.ReleaseListReleasesHandler.Handle(params, &models.Principal{})
_, ok := response.(*release.ListReleasesOK)
suite.assert.True(ok)
}
func TestReleases(t *testing.T) {
suite.Run(t, new(ReleasesTestSuite))
}

View File

@@ -1,810 +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"
"net/url"
"strconv"
"time"
"github.com/minio/console/pkg/utils"
"github.com/minio/madmin-go/v3"
"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/minio-go/v7/pkg/replication"
)
type RemoteBucketResult struct {
OriginBucket string
TargetBucket string
Error string
}
func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) {
// return list of remote buckets
api.BucketListRemoteBucketsHandler = bucketApi.ListRemoteBucketsHandlerFunc(func(params bucketApi.ListRemoteBucketsParams, session *models.Principal) middleware.Responder {
listResp, err := getListRemoteBucketsResponse(session, params)
if err != nil {
return bucketApi.NewListRemoteBucketsDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewListRemoteBucketsOK().WithPayload(listResp)
})
// return information about a specific bucket
api.BucketRemoteBucketDetailsHandler = bucketApi.RemoteBucketDetailsHandlerFunc(func(params bucketApi.RemoteBucketDetailsParams, session *models.Principal) middleware.Responder {
response, err := getRemoteBucketDetailsResponse(session, params)
if err != nil {
return bucketApi.NewRemoteBucketDetailsDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewRemoteBucketDetailsOK().WithPayload(response)
})
// delete remote bucket
api.BucketDeleteRemoteBucketHandler = bucketApi.DeleteRemoteBucketHandlerFunc(func(params bucketApi.DeleteRemoteBucketParams, session *models.Principal) middleware.Responder {
err := getDeleteRemoteBucketResponse(session, params)
if err != nil {
return bucketApi.NewDeleteRemoteBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewDeleteRemoteBucketNoContent()
})
// set remote bucket
api.BucketAddRemoteBucketHandler = bucketApi.AddRemoteBucketHandlerFunc(func(params bucketApi.AddRemoteBucketParams, session *models.Principal) middleware.Responder {
err := getAddRemoteBucketResponse(session, params)
if err != nil {
return bucketApi.NewAddRemoteBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewAddRemoteBucketCreated()
})
// set multi-bucket replication
api.BucketSetMultiBucketReplicationHandler = bucketApi.SetMultiBucketReplicationHandlerFunc(func(params bucketApi.SetMultiBucketReplicationParams, session *models.Principal) middleware.Responder {
response, err := setMultiBucketReplicationResponse(session, params)
if err != nil {
return bucketApi.NewSetMultiBucketReplicationDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewSetMultiBucketReplicationOK().WithPayload(response)
})
// list external buckets
api.BucketListExternalBucketsHandler = bucketApi.ListExternalBucketsHandlerFunc(func(params bucketApi.ListExternalBucketsParams, _ *models.Principal) middleware.Responder {
response, err := listExternalBucketsResponse(params)
if err != nil {
return bucketApi.NewListExternalBucketsDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewListExternalBucketsOK().WithPayload(response)
})
// delete replication rule
api.BucketDeleteBucketReplicationRuleHandler = bucketApi.DeleteBucketReplicationRuleHandlerFunc(func(params bucketApi.DeleteBucketReplicationRuleParams, session *models.Principal) middleware.Responder {
err := deleteReplicationRuleResponse(session, params)
if err != nil {
return bucketApi.NewDeleteBucketReplicationRuleDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewDeleteBucketReplicationRuleNoContent()
})
// delete all replication rules for a bucket
api.BucketDeleteAllReplicationRulesHandler = bucketApi.DeleteAllReplicationRulesHandlerFunc(func(params bucketApi.DeleteAllReplicationRulesParams, session *models.Principal) middleware.Responder {
err := deleteBucketReplicationRulesResponse(session, params)
if err != nil {
if err.Code == 500 && err.APIError.DetailedMessage == "The remote target does not exist" {
// We should ignore this MinIO error when deleting all replication rules
return bucketApi.NewDeleteAllReplicationRulesNoContent() // This will return 204 as per swagger spec
}
// If there is a different error, then we should handle it
// This will return a generic error with err.Code (likely a 500 or 404) and its *err.DetailedMessage
return bucketApi.NewDeleteAllReplicationRulesDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewDeleteAllReplicationRulesNoContent()
})
// delete selected replication rules for a bucket
api.BucketDeleteSelectedReplicationRulesHandler = bucketApi.DeleteSelectedReplicationRulesHandlerFunc(func(params bucketApi.DeleteSelectedReplicationRulesParams, session *models.Principal) middleware.Responder {
err := deleteSelectedReplicationRulesResponse(session, params)
if err != nil {
return bucketApi.NewDeleteSelectedReplicationRulesDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewDeleteSelectedReplicationRulesNoContent()
})
// update local bucket replication config item
api.BucketUpdateMultiBucketReplicationHandler = bucketApi.UpdateMultiBucketReplicationHandlerFunc(func(params bucketApi.UpdateMultiBucketReplicationParams, session *models.Principal) middleware.Responder {
err := updateBucketReplicationResponse(session, params)
if err != nil {
return bucketApi.NewUpdateMultiBucketReplicationDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewUpdateMultiBucketReplicationCreated()
})
}
func getListRemoteBucketsResponse(session *models.Principal, params bucketApi.ListRemoteBucketsParams) (*models.ListRemoteBucketsResponse, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
}
adminClient := AdminClient{Client: mAdmin}
return listRemoteBuckets(ctx, adminClient)
}
func getRemoteBucketDetailsResponse(session *models.Principal, params bucketApi.RemoteBucketDetailsParams) (*models.RemoteBucket, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
}
adminClient := AdminClient{Client: mAdmin}
return getRemoteBucket(ctx, adminClient, params.Name)
}
func getDeleteRemoteBucketResponse(session *models.Principal, params bucketApi.DeleteRemoteBucketParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
}
adminClient := AdminClient{Client: mAdmin}
err = deleteRemoteBucket(ctx, adminClient, params.SourceBucketName, params.Arn)
if err != nil {
return ErrorWithContext(ctx, fmt.Errorf("error deleting remote bucket: %v", err))
}
return nil
}
func getAddRemoteBucketResponse(session *models.Principal, params bucketApi.AddRemoteBucketParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
}
adminClient := AdminClient{Client: mAdmin}
_, err = addRemoteBucket(ctx, adminClient, *params.Body)
if err != nil {
return ErrorWithContext(ctx, fmt.Errorf("error adding remote bucket: %v", err))
}
return nil
}
func listRemoteBuckets(ctx context.Context, client MinioAdmin) (*models.ListRemoteBucketsResponse, *CodedAPIError) {
var remoteBuckets []*models.RemoteBucket
buckets, err := client.listRemoteBuckets(ctx, "", "")
if err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error listing remote buckets: %v", err))
}
for _, bucket := range buckets {
remoteBucket := &models.RemoteBucket{
AccessKey: swag.String(bucket.Credentials.AccessKey),
RemoteARN: swag.String(bucket.Arn),
SecretKey: bucket.Credentials.SecretKey,
Service: "replication",
SourceBucket: swag.String(bucket.SourceBucket),
Status: "",
TargetBucket: bucket.TargetBucket,
TargetURL: bucket.Endpoint,
SyncMode: "async",
Bandwidth: bucket.BandwidthLimit,
HealthCheckPeriod: int64(bucket.HealthCheckDuration.Seconds()),
}
if bucket.ReplicationSync {
remoteBucket.SyncMode = "sync"
}
remoteBuckets = append(remoteBuckets, remoteBucket)
}
return &models.ListRemoteBucketsResponse{
Buckets: remoteBuckets,
Total: int64(len(remoteBuckets)),
}, nil
}
func getRemoteBucket(ctx context.Context, client MinioAdmin, name string) (*models.RemoteBucket, *CodedAPIError) {
remoteBucket, err := client.getRemoteBucket(ctx, name, "")
if err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error getting remote bucket details: %v", err))
}
if remoteBucket == nil {
return nil, ErrorWithContext(ctx, "error getting remote bucket details: bucket not found")
}
return &models.RemoteBucket{
AccessKey: &remoteBucket.Credentials.AccessKey,
RemoteARN: &remoteBucket.Arn,
SecretKey: remoteBucket.Credentials.SecretKey,
Service: "replication",
SourceBucket: &remoteBucket.SourceBucket,
Status: "",
TargetBucket: remoteBucket.TargetBucket,
TargetURL: remoteBucket.Endpoint,
}, nil
}
func deleteRemoteBucket(ctx context.Context, client MinioAdmin, sourceBucketName, arn string) error {
return client.removeRemoteBucket(ctx, sourceBucketName, arn)
}
func addRemoteBucket(ctx context.Context, client MinioAdmin, params models.CreateRemoteBucket) (string, error) {
TargetURL := *params.TargetURL
accessKey := *params.AccessKey
secretKey := *params.SecretKey
u, err := url.Parse(TargetURL)
if err != nil {
return "", errors.New("malformed Remote target URL")
}
secure := u.Scheme == "https"
host := u.Host
if u.Port() == "" {
port := 80
if secure {
port = 443
}
host = host + ":" + strconv.Itoa(port)
}
creds := &madmin.Credentials{AccessKey: accessKey, SecretKey: secretKey}
remoteBucket := &madmin.BucketTarget{
TargetBucket: *params.TargetBucket,
Secure: secure,
Credentials: creds,
Endpoint: host,
Path: "",
API: "s3v4",
Type: "replication",
Region: params.Region,
ReplicationSync: *params.SyncMode == "sync",
}
if *params.SyncMode == "async" {
remoteBucket.BandwidthLimit = params.Bandwidth
}
if params.HealthCheckPeriod > 0 {
remoteBucket.HealthCheckDuration = time.Duration(params.HealthCheckPeriod) * time.Second
}
bucketARN, err := client.addRemoteBucket(ctx, *params.SourceBucket, remoteBucket)
return bucketARN, err
}
func addBucketReplicationItem(ctx context.Context, session *models.Principal, minClient minioClient, bucketName, prefix, destinationARN string, repExistingObj, repDelMark, repDels, repMeta bool, tags string, priority int32, storageClass string) error {
// we will tolerate this call failing
cfg, err := minClient.getBucketReplication(ctx, bucketName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error fetching replication configuration for bucket %s: %v", bucketName, err))
}
// add rule
maxPrio := 0
if priority <= 0 { // We pick next priority by default
for _, r := range cfg.Rules {
if r.Priority > maxPrio {
maxPrio = r.Priority
}
}
maxPrio++
} else { // User picked priority, we try to set this manually
maxPrio = int(priority)
}
clientIP := utils.ClientIPFromContext(ctx)
s3Client, err := newS3BucketClient(session, bucketName, prefix, clientIP)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error creating S3Client: %v", err))
return err
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
repDelMarkStatus := "disable"
if repDelMark {
repDelMarkStatus = "enable"
}
repDelsStatus := "disable"
if repDels {
repDelsStatus = "enable"
}
repMetaStatus := "disable"
if repMeta {
repMetaStatus = "enable"
}
existingRepStatus := "disable"
if repExistingObj {
existingRepStatus = "enable"
}
opts := replication.Options{
Priority: fmt.Sprintf("%d", maxPrio),
RuleStatus: "enable",
DestBucket: destinationARN,
Op: replication.AddOption,
TagString: tags,
ExistingObjectReplicate: existingRepStatus,
ReplicateDeleteMarkers: repDelMarkStatus,
ReplicateDeletes: repDelsStatus,
ReplicaSync: repMetaStatus,
StorageClass: storageClass,
}
err2 := mcClient.setReplication(ctx, &cfg, opts)
if err2 != nil {
ErrorWithContext(ctx, fmt.Errorf("error creating replication for bucket: %v", err2.Cause))
return err2.Cause
}
return nil
}
func editBucketReplicationItem(ctx context.Context, session *models.Principal, minClient minioClient, ruleID, bucketName, prefix, destinationARN string, ruleStatus, repDelMark, repDels, repMeta, existingObjectRep bool, tags string, priority int32, storageClass string) error {
// we will tolerate this call failing
cfg, err := minClient.getBucketReplication(ctx, bucketName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error fetching replication configuration for bucket %s: %v", bucketName, err))
}
maxPrio := int(priority)
clientIP := utils.ClientIPFromContext(ctx)
s3Client, err := newS3BucketClient(session, bucketName, prefix, clientIP)
if err != nil {
return fmt.Errorf("error creating S3Client: %v", err)
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
ruleState := "disable"
if ruleStatus {
ruleState = "enable"
}
repDelMarkStatus := "disable"
if repDelMark {
repDelMarkStatus = "enable"
}
repDelsStatus := "disable"
if repDels {
repDelsStatus = "enable"
}
repMetaStatus := "disable"
if repMeta {
repMetaStatus = "enable"
}
existingRepStatus := "disable"
if existingObjectRep {
existingRepStatus = "enable"
}
opts := replication.Options{
ID: ruleID,
Priority: fmt.Sprintf("%d", maxPrio),
RuleStatus: ruleState,
DestBucket: destinationARN,
Op: replication.SetOption,
TagString: tags,
IsTagSet: true,
ExistingObjectReplicate: existingRepStatus,
ReplicateDeleteMarkers: repDelMarkStatus,
ReplicateDeletes: repDelsStatus,
ReplicaSync: repMetaStatus,
StorageClass: storageClass,
IsSCSet: true,
}
err2 := mcClient.setReplication(ctx, &cfg, opts)
if err2 != nil {
return fmt.Errorf("error modifying replication for bucket: %v", err2.Cause)
}
return nil
}
func setMultiBucketReplication(ctx context.Context, session *models.Principal, client MinioAdmin, minClient minioClient, params bucketApi.SetMultiBucketReplicationParams) []RemoteBucketResult {
bucketsRelation := params.Body.BucketsRelation
// Parallel remote bucket adding
parallelRemoteBucket := func(bucketRelationData *models.MultiBucketsRelation) chan RemoteBucketResult {
remoteProc := make(chan RemoteBucketResult)
sourceBucket := bucketRelationData.OriginBucket
targetBucket := bucketRelationData.DestinationBucket
go func() {
defer close(remoteProc)
createRemoteBucketParams := models.CreateRemoteBucket{
AccessKey: params.Body.AccessKey,
SecretKey: params.Body.SecretKey,
SourceBucket: &sourceBucket,
TargetBucket: &targetBucket,
Region: params.Body.Region,
TargetURL: params.Body.TargetURL,
SyncMode: params.Body.SyncMode,
Bandwidth: params.Body.Bandwidth,
HealthCheckPeriod: params.Body.HealthCheckPeriod,
}
// We add the remote bucket reference & store the arn or errors returned
arn, err := addRemoteBucket(ctx, client, createRemoteBucketParams)
if err == nil {
err = addBucketReplicationItem(
ctx,
session,
minClient,
sourceBucket,
params.Body.Prefix,
arn,
params.Body.ReplicateExistingObjects,
params.Body.ReplicateDeleteMarkers,
params.Body.ReplicateDeletes,
params.Body.ReplicateMetadata,
params.Body.Tags,
params.Body.Priority,
params.Body.StorageClass)
}
errorReturn := ""
if err != nil {
deleteRemoteBucket(ctx, client, sourceBucket, arn)
errorReturn = err.Error()
}
retParams := RemoteBucketResult{
OriginBucket: sourceBucket,
TargetBucket: targetBucket,
Error: errorReturn,
}
remoteProc <- retParams
}()
return remoteProc
}
var bucketsManagement []chan RemoteBucketResult
for _, bucketName := range bucketsRelation {
// We generate the ARNs for each bucket
rBucket := parallelRemoteBucket(bucketName)
bucketsManagement = append(bucketsManagement, rBucket)
}
resultsList := []RemoteBucketResult{}
for _, result := range bucketsManagement {
res := <-result
resultsList = append(resultsList, res)
}
return resultsList
}
func setMultiBucketReplicationResponse(session *models.Principal, params bucketApi.SetMultiBucketReplicationParams) (*models.MultiBucketResponseState, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
}
adminClient := AdminClient{Client: mAdmin}
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
if err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating MinIO Client: %v", err))
}
// create a minioClient interface implementation
// defining the client to be used
mnClient := minioClient{client: mClient}
replicationResults := setMultiBucketReplication(ctx, session, adminClient, mnClient, params)
if replicationResults == nil {
return nil, ErrorWithContext(ctx, errors.New("error setting buckets replication"))
}
resParsed := []*models.MultiBucketResponseItem{}
for _, repResult := range replicationResults {
responseItem := models.MultiBucketResponseItem{
ErrorString: repResult.Error,
OriginBucket: repResult.OriginBucket,
TargetBucket: repResult.TargetBucket,
}
resParsed = append(resParsed, &responseItem)
}
resultsParsed := models.MultiBucketResponseState{
ReplicationState: resParsed,
}
return &resultsParsed, nil
}
func listExternalBucketsResponse(params bucketApi.ListExternalBucketsParams) (*models.ListBucketsResponse, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
remoteAdmin, err := newAdminFromCreds(*params.Body.AccessKey, *params.Body.SecretKey, *params.Body.TargetURL, *params.Body.UseTLS)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return listExternalBuckets(ctx, AdminClient{Client: remoteAdmin})
}
func listExternalBuckets(ctx context.Context, client MinioAdmin) (*models.ListBucketsResponse, *CodedAPIError) {
buckets, err := getAccountBuckets(ctx, client)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.ListBucketsResponse{
Buckets: buckets,
Total: int64(len(buckets)),
}, nil
}
func getARNFromID(conf *replication.Config, rule string) string {
for i := range conf.Rules {
if conf.Rules[i].ID == rule {
return conf.Rules[i].Destination.Bucket
}
}
return ""
}
func getARNsFromIDs(conf *replication.Config, rules []string) []string {
temp := make(map[string]string)
for i := range conf.Rules {
temp[conf.Rules[i].ID] = conf.Rules[i].Destination.Bucket
}
var retval []string
for i := range rules {
if val, ok := temp[rules[i]]; ok {
retval = append(retval, val)
}
}
return retval
}
func deleteReplicationRule(ctx context.Context, session *models.Principal, bucketName, ruleID string) error {
clientIP := utils.ClientIPFromContext(ctx)
mClient, err := newMinioClient(session, clientIP)
if err != nil {
return fmt.Errorf("error creating MinIO Client: %v", err)
}
// create a minioClient interface implementation
// defining the client to be used
minClient := minioClient{client: mClient}
cfg, err := minClient.getBucketReplication(ctx, bucketName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
}
s3Client, err := newS3BucketClient(session, bucketName, "", clientIP)
if err != nil {
return fmt.Errorf("error creating S3Client: %v", err)
}
mAdmin, err := NewMinioAdminClient(ctx, session)
if err != nil {
return fmt.Errorf("error creating Admin Client: %v", err)
}
admClient := AdminClient{Client: mAdmin}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
opts := replication.Options{
ID: ruleID,
Op: replication.RemoveOption,
}
err2 := mcClient.setReplication(ctx, &cfg, opts)
if err2 != nil {
return err2.Cause
}
// Replication rule was successfully deleted. We remove remote bucket
err3 := deleteRemoteBucket(ctx, admClient, bucketName, getARNFromID(&cfg, ruleID))
if err3 != nil {
return err3
}
return nil
}
func deleteAllReplicationRules(ctx context.Context, session *models.Principal, bucketName string) error {
clientIP := utils.ClientIPFromContext(ctx)
s3Client, err := newS3BucketClient(session, bucketName, "", clientIP)
if err != nil {
return fmt.Errorf("error creating S3Client: %v", err)
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
mClient, err := newMinioClient(session, clientIP)
if err != nil {
return fmt.Errorf("error creating MinIO Client: %v", err)
}
// create a minioClient interface implementation
// defining the client to be used
minClient := minioClient{client: mClient}
cfg, err := minClient.getBucketReplication(ctx, bucketName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
}
mAdmin, err := NewMinioAdminClient(ctx, session)
if err != nil {
return fmt.Errorf("error creating Admin Client: %v", err)
}
admClient := AdminClient{Client: mAdmin}
err2 := mcClient.deleteAllReplicationRules(ctx)
if err2 != nil {
return err2.ToGoError()
}
for i := range cfg.Rules {
err3 := deleteRemoteBucket(ctx, admClient, bucketName, cfg.Rules[i].Destination.Bucket)
if err3 != nil {
return err3
}
}
return nil
}
func deleteSelectedReplicationRules(ctx context.Context, session *models.Principal, bucketName string, rules []string) error {
clientIP := utils.ClientIPFromContext(ctx)
mClient, err := newMinioClient(session, clientIP)
if err != nil {
return fmt.Errorf("error creating MinIO Client: %v", err)
}
// create a minioClient interface implementation
// defining the client to be used
minClient := minioClient{client: mClient}
cfg, err := minClient.getBucketReplication(ctx, bucketName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
}
s3Client, err := newS3BucketClient(session, bucketName, "", clientIP)
if err != nil {
return fmt.Errorf("error creating S3Client: %v", err)
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
mAdmin, err := NewMinioAdminClient(ctx, session)
if err != nil {
return fmt.Errorf("error creating Admin Client: %v", err)
}
admClient := AdminClient{Client: mAdmin}
ARNs := getARNsFromIDs(&cfg, rules)
for i := range rules {
opts := replication.Options{
ID: rules[i],
Op: replication.RemoveOption,
}
err2 := mcClient.setReplication(ctx, &cfg, opts)
if err2 != nil {
return err2.Cause
}
// In case replication rule was deleted successfully, we remove the remote bucket ARN
err3 := deleteRemoteBucket(ctx, admClient, bucketName, ARNs[i])
if err3 != nil {
return err3
}
}
return nil
}
func deleteReplicationRuleResponse(session *models.Principal, params bucketApi.DeleteBucketReplicationRuleParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
err := deleteReplicationRule(ctx, session, params.BucketName, params.RuleID)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func deleteBucketReplicationRulesResponse(session *models.Principal, params bucketApi.DeleteAllReplicationRulesParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
err := deleteAllReplicationRules(ctx, session, params.BucketName)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func deleteSelectedReplicationRulesResponse(session *models.Principal, params bucketApi.DeleteSelectedReplicationRulesParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
err := deleteSelectedReplicationRules(ctx, session, params.BucketName, params.Rules.Rules)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func updateBucketReplicationResponse(session *models.Principal, params bucketApi.UpdateMultiBucketReplicationParams) *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
minClient := minioClient{client: mClient}
err = editBucketReplicationItem(
ctx,
session,
minClient,
params.RuleID,
params.BucketName,
params.Body.Prefix,
params.Body.Arn,
params.Body.RuleState,
params.Body.ReplicateDeleteMarkers,
params.Body.ReplicateDeletes,
params.Body.ReplicateMetadata,
params.Body.ReplicateExistingObjects,
params.Body.Tags,
params.Body.Priority,
params.Body.StorageClass)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}

View File

@@ -1,386 +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"
"errors"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/minio/console/pkg/utils"
"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/madmin-go/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type RemoteBucketsTestSuite struct {
suite.Suite
assert *assert.Assertions
currentServer string
isServerSet bool
server *httptest.Server
adminClient AdminClientMock
minioClient minioClientMock
mockRemoteBucket *models.RemoteBucket
mockBucketTarget *madmin.BucketTarget
mockListBuckets *models.ListBucketsResponse
}
func (suite *RemoteBucketsTestSuite) SetupSuite() {
suite.assert = assert.New(suite.T())
suite.adminClient = AdminClientMock{}
suite.minioClient = minioClientMock{}
suite.mockObjects()
}
func (suite *RemoteBucketsTestSuite) mockObjects() {
suite.mockListBuckets = &models.ListBucketsResponse{
Buckets: []*models.Bucket{},
Total: 0,
}
suite.mockRemoteBucket = &models.RemoteBucket{
AccessKey: swag.String("accessKey"),
SecretKey: "secretKey",
RemoteARN: swag.String("remoteARN"),
Service: "replication",
SourceBucket: swag.String("sourceBucket"),
TargetBucket: "targetBucket",
TargetURL: "targetURL",
Status: "",
}
suite.mockBucketTarget = &madmin.BucketTarget{
Credentials: &madmin.Credentials{
AccessKey: *suite.mockRemoteBucket.AccessKey,
SecretKey: suite.mockRemoteBucket.SecretKey,
},
Arn: *suite.mockRemoteBucket.RemoteARN,
SourceBucket: *suite.mockRemoteBucket.SourceBucket,
TargetBucket: suite.mockRemoteBucket.TargetBucket,
Endpoint: suite.mockRemoteBucket.TargetURL,
}
}
func (suite *RemoteBucketsTestSuite) SetupTest() {
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
os.Setenv(ConsoleMinIOServer, suite.server.URL)
}
func (suite *RemoteBucketsTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(400)
}
func (suite *RemoteBucketsTestSuite) TearDownSuite() {
}
func (suite *RemoteBucketsTestSuite) TearDownTest() {
if suite.isServerSet {
os.Setenv(ConsoleMinIOServer, suite.currentServer)
} else {
os.Unsetenv(ConsoleMinIOServer)
}
}
func (suite *RemoteBucketsTestSuite) TestRegisterRemoteBucketsHandlers() {
api := &operations.ConsoleAPI{}
suite.assertHandlersAreNil(api)
registerAdminBucketRemoteHandlers(api)
suite.assertHandlersAreNotNil(api)
}
func (suite *RemoteBucketsTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
suite.assert.Nil(api.BucketListRemoteBucketsHandler)
suite.assert.Nil(api.BucketRemoteBucketDetailsHandler)
suite.assert.Nil(api.BucketDeleteRemoteBucketHandler)
suite.assert.Nil(api.BucketAddRemoteBucketHandler)
suite.assert.Nil(api.BucketSetMultiBucketReplicationHandler)
suite.assert.Nil(api.BucketListExternalBucketsHandler)
suite.assert.Nil(api.BucketDeleteBucketReplicationRuleHandler)
suite.assert.Nil(api.BucketDeleteAllReplicationRulesHandler)
suite.assert.Nil(api.BucketDeleteSelectedReplicationRulesHandler)
suite.assert.Nil(api.BucketUpdateMultiBucketReplicationHandler)
}
func (suite *RemoteBucketsTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
suite.assert.NotNil(api.BucketListRemoteBucketsHandler)
suite.assert.NotNil(api.BucketRemoteBucketDetailsHandler)
suite.assert.NotNil(api.BucketDeleteRemoteBucketHandler)
suite.assert.NotNil(api.BucketAddRemoteBucketHandler)
suite.assert.NotNil(api.BucketSetMultiBucketReplicationHandler)
suite.assert.NotNil(api.BucketListExternalBucketsHandler)
suite.assert.NotNil(api.BucketDeleteBucketReplicationRuleHandler)
suite.assert.NotNil(api.BucketDeleteAllReplicationRulesHandler)
suite.assert.NotNil(api.BucketDeleteSelectedReplicationRulesHandler)
suite.assert.NotNil(api.BucketUpdateMultiBucketReplicationHandler)
}
func (suite *RemoteBucketsTestSuite) TestListRemoteBucketsHandlerWithError() {
params, api := suite.initListRemoteBucketsRequest()
response := api.BucketListRemoteBucketsHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.ListRemoteBucketsDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initListRemoteBucketsRequest() (params bucketApi.ListRemoteBucketsParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestListRemoteBucketsWithoutError() {
ctx := context.Background()
minioListRemoteBucketsMock = func(_ context.Context, _, _ string) (targets []madmin.BucketTarget, err error) {
return []madmin.BucketTarget{{
Credentials: &madmin.Credentials{
AccessKey: "accessKey",
SecretKey: "secretKey",
},
}}, nil
}
res, err := listRemoteBuckets(ctx, &suite.adminClient)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *RemoteBucketsTestSuite) TestRemoteBucketDetailsHandlerWithError() {
params, api := suite.initRemoteBucketDetailsRequest()
response := api.BucketRemoteBucketDetailsHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.RemoteBucketDetailsDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initRemoteBucketDetailsRequest() (params bucketApi.RemoteBucketDetailsParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestGetRemoteBucketWithoutError() {
ctx := context.Background()
minioGetRemoteBucketMock = func(_ context.Context, _, _ string) (targets *madmin.BucketTarget, err error) {
return suite.mockBucketTarget, nil
}
res, err := getRemoteBucket(ctx, &suite.adminClient, "bucketName")
suite.assert.Nil(err)
suite.assert.NotNil(res)
suite.assert.Equal(suite.mockRemoteBucket, res)
}
func (suite *RemoteBucketsTestSuite) TestDeleteRemoteBucketHandlerWithError() {
params, api := suite.initDeleteRemoteBucketRequest()
response := api.BucketDeleteRemoteBucketHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.DeleteRemoteBucketDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initDeleteRemoteBucketRequest() (params bucketApi.DeleteRemoteBucketParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestAddRemoteBucketHandlerWithError() {
params, api := suite.initAddRemoteBucketRequest()
response := api.BucketAddRemoteBucketHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.AddRemoteBucketDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initAddRemoteBucketRequest() (params bucketApi.AddRemoteBucketParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
url := "^&*&^%^"
accessKey := "accessKey"
secretKey := "secretKey"
params.HTTPRequest = &http.Request{}
params.Body = &models.CreateRemoteBucket{
TargetURL: &url,
AccessKey: &accessKey,
SecretKey: &secretKey,
}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestAddRemoteBucketWithoutError() {
ctx := context.Background()
minioAddRemoteBucketMock = func(_ context.Context, _ string, _ *madmin.BucketTarget) (string, error) {
return "bucketName", nil
}
url := "https://localhost"
accessKey := "accessKey"
secretKey := "secretKey"
targetBucket := "targetBucket"
syncMode := "async"
sourceBucket := "sourceBucket"
data := models.CreateRemoteBucket{
TargetURL: &url,
TargetBucket: &targetBucket,
AccessKey: &accessKey,
SecretKey: &secretKey,
SyncMode: &syncMode,
HealthCheckPeriod: 10,
SourceBucket: &sourceBucket,
}
res, err := addRemoteBucket(ctx, &suite.adminClient, data)
suite.assert.NotNil(res)
suite.assert.Nil(err)
}
func (suite *RemoteBucketsTestSuite) TestSetMultiBucketReplicationHandlerWithError() {
params, api := suite.initSetMultiBucketReplicationRequest()
response := api.BucketSetMultiBucketReplicationHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.SetMultiBucketReplicationOK)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initSetMultiBucketReplicationRequest() (params bucketApi.SetMultiBucketReplicationParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
accessKey := "accessKey"
secretKey := "secretKey"
targetURL := "https://localhost"
syncMode := "async"
params.HTTPRequest = &http.Request{}
params.Body = &models.MultiBucketReplication{
BucketsRelation: []*models.MultiBucketsRelation{{}},
AccessKey: &accessKey,
SecretKey: &secretKey,
Region: "region",
TargetURL: &targetURL,
SyncMode: &syncMode,
Bandwidth: 10,
HealthCheckPeriod: 10,
}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestListExternalBucketsHandlerWithError() {
params, api := suite.initListExternalBucketsRequest()
response := api.BucketListExternalBucketsHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.ListExternalBucketsDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initListExternalBucketsRequest() (params bucketApi.ListExternalBucketsParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
url := "http://localhost:9000"
accessKey := "accessKey"
secretKey := "secretKey"
tls := false
params.HTTPRequest = &http.Request{}
params.Body = &models.ListExternalBucketsParams{
TargetURL: &url,
AccessKey: &accessKey,
SecretKey: &secretKey,
UseTLS: &tls,
}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestListExternalBucketsWithError() {
ctx := context.Background()
minioAccountInfoMock = func(_ context.Context) (madmin.AccountInfo, error) {
return madmin.AccountInfo{}, errors.New("error")
}
res, err := listExternalBuckets(ctx, &suite.adminClient)
suite.assert.NotNil(err)
suite.assert.Nil(res)
}
func (suite *RemoteBucketsTestSuite) TestListExternalBucketsWithoutError() {
ctx := context.Background()
minioAccountInfoMock = func(_ context.Context) (madmin.AccountInfo, error) {
return madmin.AccountInfo{
Buckets: []madmin.BucketAccessInfo{},
}, nil
}
res, err := listExternalBuckets(ctx, &suite.adminClient)
suite.assert.Nil(err)
suite.assert.NotNil(res)
suite.assert.Equal(suite.mockListBuckets, res)
}
func (suite *RemoteBucketsTestSuite) TestDeleteBucketReplicationRuleHandlerWithError() {
params, api := suite.initDeleteBucketReplicationRuleRequest()
response := api.BucketDeleteBucketReplicationRuleHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.DeleteBucketReplicationRuleDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initDeleteBucketReplicationRuleRequest() (params bucketApi.DeleteBucketReplicationRuleParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestDeleteAllReplicationRulesHandlerWithError() {
params, api := suite.initDeleteAllReplicationRulesRequest()
response := api.BucketDeleteAllReplicationRulesHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.DeleteAllReplicationRulesDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initDeleteAllReplicationRulesRequest() (params bucketApi.DeleteAllReplicationRulesParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestDeleteSelectedReplicationRulesHandlerWithError() {
params, api := suite.initDeleteSelectedReplicationRulesRequest()
response := api.BucketDeleteSelectedReplicationRulesHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.DeleteSelectedReplicationRulesDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initDeleteSelectedReplicationRulesRequest() (params bucketApi.DeleteSelectedReplicationRulesParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
params.HTTPRequest = &http.Request{}
params.BucketName = "bucketName"
params.Rules = &models.BucketReplicationRuleList{
Rules: []string{"rule1", "rule2"},
}
return params, api
}
func (suite *RemoteBucketsTestSuite) TestUpdateMultiBucketReplicationHandlerWithError() {
params, api := suite.initUpdateMultiBucketReplicationRequest()
response := api.BucketUpdateMultiBucketReplicationHandler.Handle(params, &models.Principal{})
_, ok := response.(*bucketApi.UpdateMultiBucketReplicationDefault)
suite.assert.True(ok)
}
func (suite *RemoteBucketsTestSuite) initUpdateMultiBucketReplicationRequest() (params bucketApi.UpdateMultiBucketReplicationParams, api operations.ConsoleAPI) {
registerAdminBucketRemoteHandlers(&api)
r := &http.Request{}
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
rc := r.WithContext(ctx)
params.HTTPRequest = rc
params.Body = &models.MultiBucketReplicationEdit{}
return params, api
}
func TestRemoteBuckets(t *testing.T) {
suite.Run(t, new(RemoteBucketsTestSuite))
}

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 api
import (
"context"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
siteRepApi "github.com/minio/console/api/operations/site_replication"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
)
func registerSiteReplicationStatusHandler(api *operations.ConsoleAPI) {
api.SiteReplicationGetSiteReplicationStatusHandler = siteRepApi.GetSiteReplicationStatusHandlerFunc(func(params siteRepApi.GetSiteReplicationStatusParams, session *models.Principal) middleware.Responder {
rInfo, err := getSRStatusResponse(session, params)
if err != nil {
return siteRepApi.NewGetSiteReplicationStatusDefault(err.Code).WithPayload(err.APIError)
}
return siteRepApi.NewGetSiteReplicationStatusOK().WithPayload(rInfo)
})
}
func getSRStatusResponse(session *models.Principal, params siteRepApi.GetSiteReplicationStatusParams) (*models.SiteReplicationStatusResponse, *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)
}
adminClient := AdminClient{Client: mAdmin}
res, err := getSRStats(ctx, adminClient, params)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return res, nil
}
func getSRStats(ctx context.Context, client MinioAdmin, params siteRepApi.GetSiteReplicationStatusParams) (info *models.SiteReplicationStatusResponse, err error) {
srParams := madmin.SRStatusOptions{
Buckets: *params.Buckets,
Policies: *params.Policies,
Users: *params.Users,
Groups: *params.Groups,
}
if params.EntityType != nil && params.EntityValue != nil {
srParams.Entity = madmin.GetSREntityType(*params.EntityType)
srParams.EntityValue = *params.EntityValue
}
srInfo, err := client.getSiteReplicationStatus(ctx, srParams)
retInfo := models.SiteReplicationStatusResponse{
BucketStats: &srInfo.BucketStats,
Enabled: srInfo.Enabled,
GroupStats: srInfo.GroupStats,
MaxBuckets: int64(srInfo.MaxBuckets),
MaxGroups: int64(srInfo.MaxGroups),
MaxPolicies: int64(srInfo.MaxPolicies),
MaxUsers: int64(srInfo.MaxUsers),
PolicyStats: &srInfo.PolicyStats,
Sites: &srInfo.Sites,
StatsSummary: srInfo.StatsSummary,
UserStats: &srInfo.UserStats,
}
if err != nil {
return nil, err
}
return &retInfo, nil
}

View File

@@ -1,242 +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"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
siteRepApi "github.com/minio/console/api/operations/site_replication"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
)
func registerSiteReplicationHandler(api *operations.ConsoleAPI) {
api.SiteReplicationGetSiteReplicationInfoHandler = siteRepApi.GetSiteReplicationInfoHandlerFunc(func(params siteRepApi.GetSiteReplicationInfoParams, session *models.Principal) middleware.Responder {
rInfo, err := getSRInfoResponse(session, params)
if err != nil {
return siteRepApi.NewGetSiteReplicationInfoDefault(err.Code).WithPayload(err.APIError)
}
return siteRepApi.NewGetSiteReplicationInfoOK().WithPayload(rInfo)
})
api.SiteReplicationSiteReplicationInfoAddHandler = siteRepApi.SiteReplicationInfoAddHandlerFunc(func(params siteRepApi.SiteReplicationInfoAddParams, session *models.Principal) middleware.Responder {
eInfo, err := getSRAddResponse(session, params)
if err != nil {
return siteRepApi.NewSiteReplicationInfoAddDefault(err.Code).WithPayload(err.APIError)
}
return siteRepApi.NewSiteReplicationInfoAddOK().WithPayload(eInfo)
})
api.SiteReplicationSiteReplicationRemoveHandler = siteRepApi.SiteReplicationRemoveHandlerFunc(func(params siteRepApi.SiteReplicationRemoveParams, session *models.Principal) middleware.Responder {
remRes, err := getSRRemoveResponse(session, params)
if err != nil {
return siteRepApi.NewSiteReplicationRemoveDefault(err.Code).WithPayload(err.APIError)
}
return siteRepApi.NewSiteReplicationRemoveNoContent().WithPayload(remRes)
})
api.SiteReplicationSiteReplicationEditHandler = siteRepApi.SiteReplicationEditHandlerFunc(func(params siteRepApi.SiteReplicationEditParams, session *models.Principal) middleware.Responder {
eInfo, err := getSREditResponse(session, params)
if err != nil {
return siteRepApi.NewSiteReplicationRemoveDefault(err.Code).WithPayload(err.APIError)
}
return siteRepApi.NewSiteReplicationEditOK().WithPayload(eInfo)
})
}
func getSRInfoResponse(session *models.Principal, params siteRepApi.GetSiteReplicationInfoParams) (*models.SiteReplicationInfoResponse, *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)
}
adminClient := AdminClient{Client: mAdmin}
res, err := getSRConfig(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return res, nil
}
func getSRAddResponse(session *models.Principal, params siteRepApi.SiteReplicationInfoAddParams) (*models.SiteReplicationAddResponse, *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)
}
adminClient := AdminClient{Client: mAdmin}
res, err := addSiteReplication(ctx, adminClient, &params)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return res, nil
}
func getSREditResponse(session *models.Principal, params siteRepApi.SiteReplicationEditParams) (*models.PeerSiteEditResponse, *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)
}
adminClient := AdminClient{Client: mAdmin}
eRes, err := editSiteReplication(ctx, adminClient, &params)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return eRes, nil
}
func getSRRemoveResponse(session *models.Principal, params siteRepApi.SiteReplicationRemoveParams) (*models.PeerSiteRemoveResponse, *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)
}
adminClient := AdminClient{Client: mAdmin}
rRes, err := removeSiteReplication(ctx, adminClient, &params)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return rRes, nil
}
func getSRConfig(ctx context.Context, client MinioAdmin) (info *models.SiteReplicationInfoResponse, err error) {
srInfo, err := client.getSiteReplicationInfo(ctx)
if err != nil {
return nil, err
}
var sites []*models.PeerInfo
if len(srInfo.Sites) > 0 {
for _, s := range srInfo.Sites {
pInfo := &models.PeerInfo{
DeploymentID: s.DeploymentID,
Endpoint: s.Endpoint,
Name: s.Name,
}
sites = append(sites, pInfo)
}
}
res := &models.SiteReplicationInfoResponse{
Enabled: srInfo.Enabled,
Name: srInfo.Name,
ServiceAccountAccessKey: srInfo.ServiceAccountAccessKey,
Sites: sites,
}
return res, nil
}
func addSiteReplication(ctx context.Context, client MinioAdmin, params *siteRepApi.SiteReplicationInfoAddParams) (info *models.SiteReplicationAddResponse, err error) {
var rSites []madmin.PeerSite
if len(params.Body) > 0 {
for _, aSite := range params.Body {
pInfo := &madmin.PeerSite{
AccessKey: aSite.AccessKey,
Name: aSite.Name,
SecretKey: aSite.SecretKey,
Endpoint: aSite.Endpoint,
}
rSites = append(rSites, *pInfo)
}
}
qs := runtime.Values(params.HTTPRequest.URL.Query())
_, qhkReplicateILMExpiry, _ := qs.GetOK("replicate-ilm-expiry")
var opts madmin.SRAddOptions
if qhkReplicateILMExpiry {
opts.ReplicateILMExpiry = true
}
cc, err := client.addSiteReplicationInfo(ctx, rSites, opts)
if err != nil {
return nil, err
}
res := &models.SiteReplicationAddResponse{
ErrorDetail: cc.ErrDetail,
InitialSyncErrorMessage: cc.InitialSyncErrorMessage,
Status: cc.Status,
Success: cc.Success,
}
return res, nil
}
func editSiteReplication(ctx context.Context, client MinioAdmin, params *siteRepApi.SiteReplicationEditParams) (info *models.PeerSiteEditResponse, err error) {
peerSiteInfo := &madmin.PeerInfo{
Endpoint: params.Body.Endpoint, // only endpoint can be edited.
Name: params.Body.Name, // does not get updated.
DeploymentID: params.Body.DeploymentID, // readonly
}
qs := runtime.Values(params.HTTPRequest.URL.Query())
_, qhkDisableILMExpiryReplication, _ := qs.GetOK("disable-ilm-expiry-replication")
_, qhkEnableILMExpiryReplication, _ := qs.GetOK("enable-ilm-expiry-replication")
var opts madmin.SREditOptions
if qhkDisableILMExpiryReplication {
opts.DisableILMExpiryReplication = true
}
if qhkEnableILMExpiryReplication {
opts.EnableILMExpiryReplication = true
}
eRes, err := client.editSiteReplicationInfo(ctx, *peerSiteInfo, opts)
if err != nil {
return nil, err
}
editRes := &models.PeerSiteEditResponse{
ErrorDetail: eRes.ErrDetail,
Status: eRes.Status,
Success: eRes.Success,
}
return editRes, nil
}
func removeSiteReplication(ctx context.Context, client MinioAdmin, params *siteRepApi.SiteReplicationRemoveParams) (info *models.PeerSiteRemoveResponse, err error) {
delAll := params.Body.All
siteNames := params.Body.Sites
var req *madmin.SRRemoveReq
if delAll {
req = &madmin.SRRemoveReq{
RemoveAll: delAll,
}
} else {
req = &madmin.SRRemoveReq{
SiteNames: siteNames,
RemoveAll: delAll,
}
}
rRes, err := client.deleteSiteReplicationInfo(ctx, *req)
if err != nil {
return nil, err
}
removeRes := &models.PeerSiteRemoveResponse{
ErrorDetail: rRes.ErrDetail,
Status: rRes.Status,
}
return removeRes, nil
}

View File

@@ -1,255 +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/>.
// These tests are for AdminAPI Tag based on swagger-console.yml
package api
import (
"context"
"fmt"
"testing"
"github.com/minio/madmin-go/v3"
"github.com/stretchr/testify/assert"
)
func TestGetSiteReplicationInfo(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "getSiteReplicationInfo()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
retValueMock := madmin.SiteReplicationInfo{
Enabled: true,
Name: "site1",
Sites: []madmin.PeerInfo{
{
Endpoint: "http://localhost:9000",
Name: "site1",
DeploymentID: "12345",
},
{
Endpoint: "http://localhost:9001",
Name: "site2",
DeploymentID: "123456",
},
},
ServiceAccountAccessKey: "test-key",
}
expValueMock := &madmin.SiteReplicationInfo{
Enabled: true,
Name: "site1",
Sites: []madmin.PeerInfo{
{
Endpoint: "http://localhost:9000",
Name: "site1",
DeploymentID: "12345",
},
{
Endpoint: "http://localhost:9001",
Name: "site2",
DeploymentID: "123456",
},
},
ServiceAccountAccessKey: "test-key",
}
getSiteReplicationInfo = func(_ context.Context) (info *madmin.SiteReplicationInfo, err error) {
return &retValueMock, nil
}
srInfo, err := adminClient.getSiteReplicationInfo(ctx)
assert.Nil(err)
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
}
func TestAddSiteReplicationInfo(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "addSiteReplicationInfo()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
retValueMock := &madmin.ReplicateAddStatus{
Success: true,
Status: "success",
ErrDetail: "",
InitialSyncErrorMessage: "",
}
expValueMock := &madmin.ReplicateAddStatus{
Success: true,
Status: "success",
ErrDetail: "",
InitialSyncErrorMessage: "",
}
addSiteReplicationInfo = func(_ context.Context, _ []madmin.PeerSite) (res *madmin.ReplicateAddStatus, err error) {
return retValueMock, nil
}
sites := []madmin.PeerSite{
{
Name: "site1",
Endpoint: "http://localhost:9000",
AccessKey: "test",
SecretKey: "test",
},
{
Name: "site2",
Endpoint: "http://localhost:9001",
AccessKey: "test",
SecretKey: "test",
},
}
srInfo, err := adminClient.addSiteReplicationInfo(ctx, sites, madmin.SRAddOptions{})
assert.Nil(err)
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
}
func TestEditSiteReplicationInfo(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "editSiteReplicationInfo()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
retValueMock := &madmin.ReplicateEditStatus{
Success: true,
Status: "success",
ErrDetail: "",
}
expValueMock := &madmin.ReplicateEditStatus{
Success: true,
Status: "success",
ErrDetail: "",
}
editSiteReplicationInfo = func(_ context.Context, _ madmin.PeerInfo) (res *madmin.ReplicateEditStatus, err error) {
return retValueMock, nil
}
site := madmin.PeerInfo{
Name: "",
Endpoint: "",
DeploymentID: "12345",
}
srInfo, err := adminClient.editSiteReplicationInfo(ctx, site, madmin.SREditOptions{})
assert.Nil(err)
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
}
func TestDeleteSiteReplicationInfo(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "deleteSiteReplicationInfo()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
retValueMock := &madmin.ReplicateRemoveStatus{
Status: "success",
ErrDetail: "",
}
expValueMock := &madmin.ReplicateRemoveStatus{
Status: "success",
ErrDetail: "",
}
deleteSiteReplicationInfoMock = func(_ context.Context, _ madmin.SRRemoveReq) (res *madmin.ReplicateRemoveStatus, err error) {
return retValueMock, nil
}
remReq := madmin.SRRemoveReq{
SiteNames: []string{
"test1",
},
RemoveAll: false,
}
srInfo, err := adminClient.deleteSiteReplicationInfo(ctx, remReq)
assert.Nil(err)
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
}
func TestSiteReplicationStatus(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "getSiteReplicationStatus()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
retValueMock := madmin.SRStatusInfo{
Enabled: true,
MaxBuckets: 0,
MaxUsers: 0,
MaxGroups: 0,
MaxPolicies: 0,
Sites: nil,
StatsSummary: nil,
BucketStats: nil,
PolicyStats: nil,
UserStats: nil,
GroupStats: nil,
}
expValueMock := &madmin.SRStatusInfo{
Enabled: true,
MaxBuckets: 0,
MaxUsers: 0,
MaxGroups: 0,
MaxPolicies: 0,
Sites: nil,
StatsSummary: nil,
BucketStats: nil,
PolicyStats: nil,
UserStats: nil,
GroupStats: nil,
}
getSiteReplicationStatus = func(_ context.Context, _ madmin.SRStatusOptions) (info *madmin.SRStatusInfo, err error) {
return &retValueMock, nil
}
reqValues := madmin.SRStatusOptions{
Buckets: true,
Policies: true,
Users: true,
Groups: true,
}
srInfo, err := adminClient.getSiteReplicationStatus(ctx, reqValues)
if err != nil {
assert.Error(err)
}
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: expected result is not same", function))
}

View File

@@ -1,118 +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"
"fmt"
"net/http"
"strconv"
"time"
"github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v3"
"github.com/minio/websocket"
)
// getSpeedtesthOptionsFromReq gets duration, size & concurrent requests from a websocket
// path come as : `/speedtest?duration=2h&size=12MiB&concurrent=10`
func getSpeedtestOptionsFromReq(req *http.Request) (*madmin.SpeedtestOpts, error) {
optionsSet := madmin.SpeedtestOpts{}
queryPairs := req.URL.Query()
paramDuration := queryPairs.Get("duration")
if paramDuration == "" {
paramDuration = "10s"
}
duration, err := time.ParseDuration(paramDuration)
if err != nil {
return nil, fmt.Errorf("unable to parse duration: %s", paramDuration)
}
if duration <= 0 {
return nil, fmt.Errorf("duration cannot be 0 or negative")
}
optionsSet.Duration = duration
paramSize := queryPairs.Get("size")
if paramSize == "" {
paramSize = "64MiB"
}
size, err := humanize.ParseBytes(paramSize)
if err != nil {
return nil, fmt.Errorf("unable to parse object size")
}
optionsSet.Size = int(size)
paramConcurrent := queryPairs.Get("concurrent")
if paramConcurrent == "" {
paramConcurrent = "32"
}
concurrent, err := strconv.Atoi(paramConcurrent)
if err != nil {
return nil, fmt.Errorf("invalid concurrent value: %s", paramConcurrent)
}
if concurrent <= 0 {
return nil, fmt.Errorf("concurrency cannot be '0' or negative")
}
optionsSet.Concurrency = concurrent
autotune := queryPairs.Get("autotune")
if autotune == "true" {
optionsSet.Autotune = true
}
return &optionsSet, nil
}
func startSpeedtest(ctx context.Context, conn WSConn, client MinioAdmin, speedtestOpts *madmin.SpeedtestOpts) error {
speedtestRes, err := client.speedtest(ctx, *speedtestOpts)
if err != nil {
LogError("error initializing speedtest: %v", err)
return err
}
for result := range speedtestRes {
// Serializing message
bytes, err := json.Marshal(result)
if err != nil {
LogError("error serializing json: %v", err)
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
LogError("error writing speedtest response: %v", err)
return err
}
}
return nil
}

View File

@@ -1,435 +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"
"net/http"
"net/url"
"os"
"github.com/minio/console/pkg/utils"
xhttp "github.com/minio/console/pkg/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
subnetApi "github.com/minio/console/api/operations/subnet"
"github.com/minio/console/models"
"github.com/minio/console/pkg/subnet"
"github.com/minio/madmin-go/v3"
)
func registerSubnetHandlers(api *operations.ConsoleAPI) {
// Get subnet login handler
api.SubnetSubnetLoginHandler = subnetApi.SubnetLoginHandlerFunc(func(params subnetApi.SubnetLoginParams, session *models.Principal) middleware.Responder {
resp, err := GetSubnetLoginResponse(session, params)
if err != nil {
return subnetApi.NewSubnetLoginDefault(err.Code).WithPayload(err.APIError)
}
return subnetApi.NewSubnetLoginOK().WithPayload(resp)
})
// Get subnet login with MFA handler
api.SubnetSubnetLoginMFAHandler = subnetApi.SubnetLoginMFAHandlerFunc(func(params subnetApi.SubnetLoginMFAParams, session *models.Principal) middleware.Responder {
resp, err := GetSubnetLoginWithMFAResponse(session, params)
if err != nil {
return subnetApi.NewSubnetLoginMFADefault(err.Code).WithPayload(err.APIError)
}
return subnetApi.NewSubnetLoginMFAOK().WithPayload(resp)
})
// Get subnet register
api.SubnetSubnetRegisterHandler = subnetApi.SubnetRegisterHandlerFunc(func(params subnetApi.SubnetRegisterParams, session *models.Principal) middleware.Responder {
err := GetSubnetRegisterResponse(session, params)
if err != nil {
return subnetApi.NewSubnetRegisterDefault(err.Code).WithPayload(err.APIError)
}
return subnetApi.NewSubnetRegisterOK()
})
// Get subnet info
api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder {
resp, err := GetSubnetInfoResponse(session, params)
if err != nil {
return subnetApi.NewSubnetInfoDefault(err.Code).WithPayload(err.APIError)
}
return subnetApi.NewSubnetInfoOK().WithPayload(resp)
})
// Get subnet registration token
api.SubnetSubnetRegTokenHandler = subnetApi.SubnetRegTokenHandlerFunc(func(params subnetApi.SubnetRegTokenParams, session *models.Principal) middleware.Responder {
resp, err := GetSubnetRegTokenResponse(session, params)
if err != nil {
return subnetApi.NewSubnetRegTokenDefault(err.Code).WithPayload(err.APIError)
}
return subnetApi.NewSubnetRegTokenOK().WithPayload(resp)
})
api.SubnetSubnetAPIKeyHandler = subnetApi.SubnetAPIKeyHandlerFunc(func(params subnetApi.SubnetAPIKeyParams, session *models.Principal) middleware.Responder {
resp, err := GetSubnetAPIKeyResponse(session, params)
if err != nil {
return subnetApi.NewSubnetAPIKeyDefault(err.Code).WithPayload(err.APIError)
}
return subnetApi.NewSubnetAPIKeyOK().WithPayload(resp)
})
}
const EnvSubnetLicense = "CONSOLE_SUBNET_LICENSE"
func SubnetRegisterWithAPIKey(ctx context.Context, minioClient MinioAdmin, apiKey string) (bool, error) {
serverInfo, err := minioClient.serverInfo(ctx)
if err != nil {
return false, err
}
clientIP := utils.ClientIPFromContext(ctx)
registerResult, err := subnet.Register(GetConsoleHTTPClient(clientIP), serverInfo, apiKey, "", "")
if err != nil {
return false, err
}
// Keep existing subnet proxy if exists
subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
if err != nil {
return false, err
}
configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
_, err = minioClient.setConfigKV(ctx, configStr)
if err != nil {
return false, err
}
// cluster registered correctly
return true, nil
}
func SubnetLogin(client xhttp.ClientI, username, password string) (string, string, error) {
tokens, err := subnet.Login(client, username, password)
if err != nil {
return "", "", err
}
if tokens.MfaToken != "" {
// user needs to complete login flow using mfa
return "", tokens.MfaToken, nil
}
if tokens.AccessToken != "" {
// register token to minio
return tokens.AccessToken, "", nil
}
return "", "", errors.New("something went wrong")
}
func GetSubnetLoginResponse(session *models.Principal, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *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)
}
return subnetLoginResponse(ctx, AdminClient{Client: mAdmin}, params)
}
func subnetLoginResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *CodedAPIError) {
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
apiKey := params.Body.APIKey
if apiKey != "" {
registered, err := SubnetRegisterWithAPIKey(ctx, minioClient, apiKey)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SubnetLoginResponse{
Registered: registered,
Organizations: []*models.SubnetOrganization{},
}, nil
}
username := params.Body.Username
password := params.Body.Password
if username != "" && password != "" {
token, mfa, err := SubnetLogin(subnetHTTPClient, username, password)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SubnetLoginResponse{
MfaToken: mfa,
AccessToken: token,
Organizations: []*models.SubnetOrganization{},
}, nil
}
return nil, ErrorWithContext(ctx, ErrDefault)
}
type SubnetRegistration struct {
AccessToken string
MFAToken string
Organizations []models.SubnetOrganization
}
func SubnetLoginWithMFA(client xhttp.ClientI, username, mfaToken, otp string) (*models.SubnetLoginResponse, error) {
tokens, err := subnet.LoginWithMFA(client, username, mfaToken, otp)
if err != nil {
return nil, err
}
if tokens.AccessToken != "" {
organizations, errOrg := subnet.GetOrganizations(client, tokens.AccessToken)
if errOrg != nil {
return nil, errOrg
}
return &models.SubnetLoginResponse{
AccessToken: tokens.AccessToken,
Organizations: organizations,
}, nil
}
return nil, errors.New("something went wrong")
}
// GetSubnetHTTPClient will return a client with proxy if configured, otherwise will return the default console http client
func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*xhttp.Client, error) {
clientIP := utils.ClientIPFromContext(ctx)
subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
if err != nil {
return nil, err
}
proxy := getSubnetProxy()
if subnetKey.Proxy != "" {
proxy = subnetKey.Proxy
}
tr := GlobalTransport.Clone()
if proxy != "" {
u, err := url.Parse(proxy)
if err != nil {
return nil, err
}
tr.Proxy = http.ProxyURL(u)
}
return &xhttp.Client{
Client: &http.Client{
Transport: &ConsoleTransport{
Transport: tr,
ClientIP: clientIP,
},
},
}, nil
}
func GetSubnetLoginWithMFAResponse(session *models.Principal, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *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)
}
minioClient := AdminClient{Client: mAdmin}
return subnetLoginWithMFAResponse(ctx, minioClient, params)
}
func subnetLoginWithMFAResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
resp, err := SubnetLoginWithMFA(subnetHTTPClient, *params.Body.Username, *params.Body.MfaToken, *params.Body.Otp)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return resp, nil
}
func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin) (*subnet.LicenseTokenConfig, error) {
buf, err := minioClient.getConfigKV(ctx, madmin.SubnetSubSys)
if err != nil {
return nil, err
}
subSysConfigs, err := madmin.ParseServerConfigOutput(string(buf))
if err != nil {
return nil, err
}
for _, scfg := range subSysConfigs {
if scfg.Target == "" {
res := subnet.LicenseTokenConfig{}
res.APIKey, _ = scfg.Lookup("api_key")
res.License, _ = scfg.Lookup("license")
res.Proxy, _ = scfg.Lookup("proxy")
return &res, nil
}
}
return nil, errors.New("unable to find subnet configuration")
}
func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient xhttp.ClientI, params subnetApi.SubnetRegisterParams) error {
serverInfo, err := minioClient.serverInfo(ctx)
if err != nil {
return err
}
registerResult, err := subnet.Register(httpClient, serverInfo, "", *params.Body.Token, *params.Body.AccountID)
if err != nil {
return err
}
// Keep existing subnet proxy if exists
subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
if err != nil {
return err
}
configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
_, err = minioClient.setConfigKV(ctx, configStr)
if err != nil {
return err
}
return nil
}
func GetSubnetRegisterResponse(session *models.Principal, params subnetApi.SubnetRegisterParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
adminClient := AdminClient{Client: mAdmin}
return subnetRegisterResponse(ctx, adminClient, params)
}
func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetRegisterParams) *CodedAPIError {
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
if err != nil {
return ErrorWithContext(ctx, err)
}
err = GetSubnetRegister(ctx, minioClient, subnetHTTPClient, params)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
var ErrSubnetLicenseNotFound = errors.New("license not found")
func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *CodedAPIError) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientIP := utils.ClientIPFromContext(ctx)
client := &xhttp.Client{
Client: GetConsoleHTTPClient(clientIP),
}
// license gets seeded to us by MinIO
seededLicense := os.Getenv(EnvSubnetLicense)
// if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
if seededLicense == "" {
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
adminClient := AdminClient{Client: mAdmin}
configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// search for licese
for _, v := range subSysConfigs {
for _, sv := range v.KV {
if sv.Key == "license" {
seededLicense = sv.Value
}
}
}
}
// still empty means not found
if seededLicense == "" {
return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
}
licenseInfo, err := getLicenseInfo(*client.Client, seededLicense)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
license := &models.License{
Email: licenseInfo.Email,
AccountID: licenseInfo.AccountID,
StorageCapacity: licenseInfo.StorageCapacity,
Plan: licenseInfo.Plan,
ExpiresAt: licenseInfo.ExpiresAt.String(),
Organization: licenseInfo.Organization,
}
return license, nil
}
func GetSubnetRegToken(ctx context.Context, minioClient MinioAdmin) (string, error) {
serverInfo, err := minioClient.serverInfo(ctx)
if err != nil {
return "", err
}
regInfo := subnet.GetClusterRegInfo(serverInfo)
regToken, err := subnet.GenerateRegToken(regInfo)
if err != nil {
return "", err
}
return regToken, nil
}
func GetSubnetRegTokenResponse(session *models.Principal, params subnetApi.SubnetRegTokenParams) (*models.SubnetRegTokenResponse, *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)
}
adminClient := AdminClient{Client: mAdmin}
return subnetRegTokenResponse(ctx, adminClient)
}
func subnetRegTokenResponse(ctx context.Context, minioClient MinioAdmin) (*models.SubnetRegTokenResponse, *CodedAPIError) {
token, err := GetSubnetRegToken(ctx, minioClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.SubnetRegTokenResponse{
RegToken: token,
}, nil
}
func GetSubnetAPIKeyResponse(session *models.Principal, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *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)
}
adminClient := AdminClient{Client: mAdmin}
return subnetAPIKeyResponse(ctx, adminClient, params)
}
func subnetAPIKeyResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
token := params.HTTPRequest.URL.Query().Get("token")
apiKey, err := subnet.GetAPIKey(subnetHTTPClient, token)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return &models.APIKey{APIKey: apiKey}, nil
}

View File

@@ -1,233 +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"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/minio/console/api/operations"
subnetApi "github.com/minio/console/api/operations/subnet"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type AdminSubnetTestSuite struct {
suite.Suite
assert *assert.Assertions
currentServer string
isServerSet bool
server *httptest.Server
adminClient AdminClientMock
}
func (suite *AdminSubnetTestSuite) SetupSuite() {
suite.assert = assert.New(suite.T())
suite.adminClient = AdminClientMock{}
minioGetConfigKVMock = func(_ string) ([]byte, error) {
return []byte("subnet license=mock api_key=mock proxy=http://mock.com"), nil
}
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
return madmin.InfoMessage{Servers: []madmin.ServerProperties{{}}}, nil
}
}
func (suite *AdminSubnetTestSuite) SetupTest() {
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
os.Setenv(ConsoleMinIOServer, suite.server.URL)
}
func (suite *AdminSubnetTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(400)
}
func (suite *AdminSubnetTestSuite) TearDownSuite() {
}
func (suite *AdminSubnetTestSuite) TearDownTest() {
if suite.isServerSet {
os.Setenv(ConsoleMinIOServer, suite.currentServer)
} else {
os.Unsetenv(ConsoleMinIOServer)
}
}
func (suite *AdminSubnetTestSuite) TestRegisterSubnetHandlers() {
api := &operations.ConsoleAPI{}
suite.assertHandlersAreNil(api)
registerSubnetHandlers(api)
suite.assertHandlersAreNotNil(api)
}
func (suite *AdminSubnetTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
suite.assert.Nil(api.SubnetSubnetLoginHandler)
suite.assert.Nil(api.SubnetSubnetLoginMFAHandler)
suite.assert.Nil(api.SubnetSubnetRegisterHandler)
suite.assert.Nil(api.SubnetSubnetInfoHandler)
suite.assert.Nil(api.SubnetSubnetRegTokenHandler)
suite.assert.Nil(api.SubnetSubnetAPIKeyHandler)
}
func (suite *AdminSubnetTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
suite.assert.NotNil(api.SubnetSubnetLoginHandler)
suite.assert.NotNil(api.SubnetSubnetLoginMFAHandler)
suite.assert.NotNil(api.SubnetSubnetRegisterHandler)
suite.assert.NotNil(api.SubnetSubnetInfoHandler)
suite.assert.NotNil(api.SubnetSubnetRegTokenHandler)
suite.assert.NotNil(api.SubnetSubnetAPIKeyHandler)
}
func (suite *AdminSubnetTestSuite) TestSubnetLoginWithSubnetClientError() {
params, api := suite.initSubnetLoginRequest("", "", "")
response := api.SubnetSubnetLoginHandler.Handle(params, &models.Principal{})
_, ok := response.(*subnetApi.SubnetLoginDefault)
suite.assert.True(ok)
}
func (suite *AdminSubnetTestSuite) TestSubnetLoginResponseWithApiKeyError() {
params, _ := suite.initSubnetLoginRequest("mock", "", "")
res, err := subnetLoginResponse(context.TODO(), suite.adminClient, params)
suite.assert.NotNil(err)
suite.assert.Nil(res)
}
func (suite *AdminSubnetTestSuite) TestSubnetLoginResponseWithCredentialsError() {
params, _ := suite.initSubnetLoginRequest("", "mock", "mock")
res, err := subnetLoginResponse(context.TODO(), suite.adminClient, params)
suite.assert.NotNil(err)
suite.assert.Nil(res)
}
func (suite *AdminSubnetTestSuite) initSubnetLoginRequest(apiKey, username, password string) (params subnetApi.SubnetLoginParams, api operations.ConsoleAPI) {
registerSubnetHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Body = &models.SubnetLoginRequest{}
params.Body.APIKey = apiKey
params.Body.Username = username
params.Body.Password = password
return params, api
}
func (suite *AdminSubnetTestSuite) TestSubnetLoginMFAWithSubnetClientError() {
params, api := suite.initSubnetLoginMFARequest("", "", "")
response := api.SubnetSubnetLoginMFAHandler.Handle(params, &models.Principal{})
_, ok := response.(*subnetApi.SubnetLoginMFADefault)
suite.assert.True(ok)
}
func (suite *AdminSubnetTestSuite) TestSubnetLoginWithMFAResponseError() {
params, _ := suite.initSubnetLoginMFARequest("mock", "mock", "mock")
res, err := subnetLoginWithMFAResponse(context.TODO(), suite.adminClient, params)
suite.assert.NotNil(err)
suite.assert.Nil(res)
}
func (suite *AdminSubnetTestSuite) initSubnetLoginMFARequest(username, mfaToken, otp string) (params subnetApi.SubnetLoginMFAParams, api operations.ConsoleAPI) {
registerSubnetHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Body = &models.SubnetLoginMFARequest{}
params.Body.Username = &username
params.Body.MfaToken = &mfaToken
params.Body.Otp = &otp
return params, api
}
func (suite *AdminSubnetTestSuite) TestSubnetRegisterClientError() {
params, api := suite.initSubnetRegisterRequest("", "")
response := api.SubnetSubnetRegisterHandler.Handle(params, &models.Principal{})
_, ok := response.(*subnetApi.SubnetRegisterDefault)
suite.assert.True(ok)
}
func (suite *AdminSubnetTestSuite) TestSubnetRegisterResponseError() {
params, _ := suite.initSubnetRegisterRequest("mock", "mock")
err := subnetRegisterResponse(context.TODO(), suite.adminClient, params)
suite.assert.NotNil(err)
}
func (suite *AdminSubnetTestSuite) initSubnetRegisterRequest(token, accountID string) (params subnetApi.SubnetRegisterParams, api operations.ConsoleAPI) {
registerSubnetHandlers(&api)
params.HTTPRequest = &http.Request{}
params.Body = &models.SubnetRegisterRequest{}
params.Body.Token = &token
params.Body.AccountID = &accountID
return params, api
}
func (suite *AdminSubnetTestSuite) TestSubnetInfoError() {
params, api := suite.initSubnetInfoRequest()
response := api.SubnetSubnetInfoHandler.Handle(params, &models.Principal{})
_, ok := response.(*subnetApi.SubnetInfoDefault)
suite.assert.True(ok)
}
func (suite *AdminSubnetTestSuite) initSubnetInfoRequest() (params subnetApi.SubnetInfoParams, api operations.ConsoleAPI) {
registerSubnetHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *AdminSubnetTestSuite) TestSubnetRegTokenError() {
params, api := suite.initSubnetRegTokenRequest()
response := api.SubnetSubnetRegTokenHandler.Handle(params, &models.Principal{})
_, ok := response.(*subnetApi.SubnetRegTokenDefault)
suite.assert.True(ok)
}
func (suite *AdminSubnetTestSuite) TestSubnetRegTokenResponse() {
res, err := subnetRegTokenResponse(context.TODO(), suite.adminClient)
suite.assert.Nil(err)
suite.assert.NotEqual("", res)
}
func (suite *AdminSubnetTestSuite) initSubnetRegTokenRequest() (params subnetApi.SubnetRegTokenParams, api operations.ConsoleAPI) {
registerSubnetHandlers(&api)
params.HTTPRequest = &http.Request{}
return params, api
}
func (suite *AdminSubnetTestSuite) TestSubnetAPIKeyWithClientError() {
params, api := suite.initSubnetAPIKeyRequest()
response := api.SubnetSubnetAPIKeyHandler.Handle(params, &models.Principal{})
_, ok := response.(*subnetApi.SubnetAPIKeyDefault)
suite.assert.True(ok)
}
func (suite *AdminSubnetTestSuite) TestSubnetAPIKeyResponseError() {
params, _ := suite.initSubnetAPIKeyRequest()
res, err := subnetAPIKeyResponse(context.TODO(), suite.adminClient, params)
suite.assert.NotNil(err)
suite.assert.Nil(res)
}
func (suite *AdminSubnetTestSuite) initSubnetAPIKeyRequest() (params subnetApi.SubnetAPIKeyParams, api operations.ConsoleAPI) {
registerSubnetHandlers(&api)
params.HTTPRequest = &http.Request{}
params.HTTPRequest.URL = &url.URL{}
return params, api
}
func TestAdminSubnet(t *testing.T) {
suite.Run(t, new(AdminSubnetTestSuite))
}

View File

@@ -1,488 +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"
"strconv"
"github.com/dustin/go-humanize"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
"github.com/minio/console/api/operations/tiering"
tieringApi "github.com/minio/console/api/operations/tiering"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
)
func registerAdminTiersHandlers(api *operations.ConsoleAPI) {
// return a list of notification endpoints
api.TieringTiersListHandler = tieringApi.TiersListHandlerFunc(func(params tieringApi.TiersListParams, session *models.Principal) middleware.Responder {
tierList, err := getTiersResponse(session, params)
if err != nil {
return tieringApi.NewTiersListDefault(err.Code).WithPayload(err.APIError)
}
return tieringApi.NewTiersListOK().WithPayload(tierList)
})
api.TieringTiersListNamesHandler = tiering.TiersListNamesHandlerFunc(func(params tiering.TiersListNamesParams, session *models.Principal) middleware.Responder {
tierList, err := getTiersNameResponse(session, params)
if err != nil {
return tieringApi.NewTiersListDefault(err.Code).WithPayload(err.APIError)
}
return tieringApi.NewTiersListNamesOK().WithPayload(tierList)
})
// add a new tiers
api.TieringAddTierHandler = tieringApi.AddTierHandlerFunc(func(params tieringApi.AddTierParams, session *models.Principal) middleware.Responder {
err := getAddTierResponse(session, params)
if err != nil {
return tieringApi.NewAddTierDefault(err.Code).WithPayload(err.APIError)
}
return tieringApi.NewAddTierCreated()
})
// get a tier
api.TieringGetTierHandler = tieringApi.GetTierHandlerFunc(func(params tieringApi.GetTierParams, session *models.Principal) middleware.Responder {
notifEndpoints, err := getGetTierResponse(session, params)
if err != nil {
return tieringApi.NewGetTierDefault(err.Code).WithPayload(err.APIError)
}
return tieringApi.NewGetTierOK().WithPayload(notifEndpoints)
})
// edit credentials for a tier
api.TieringEditTierCredentialsHandler = tieringApi.EditTierCredentialsHandlerFunc(func(params tieringApi.EditTierCredentialsParams, session *models.Principal) middleware.Responder {
err := getEditTierCredentialsResponse(session, params)
if err != nil {
return tieringApi.NewEditTierCredentialsDefault(err.Code).WithPayload(err.APIError)
}
return tieringApi.NewEditTierCredentialsOK()
})
// remove an empty tier
api.TieringRemoveTierHandler = tieringApi.RemoveTierHandlerFunc(func(params tieringApi.RemoveTierParams, session *models.Principal) middleware.Responder {
err := getRemoveTierResponse(session, params)
if err != nil {
return tieringApi.NewRemoveTierDefault(err.Code).WithPayload(err.APIError)
}
return tieringApi.NewRemoveTierNoContent()
})
}
// getTiers returns a list of tiers with their stats
func getTiers(ctx context.Context, client MinioAdmin) (*models.TierListResponse, error) {
tiers, err := client.listTiers(ctx)
if err != nil {
return nil, err
}
tierStatsInfo, err := client.tierStats(ctx)
if err != nil {
return nil, err
}
tiersStatsMap := make(map[string]madmin.TierStats, len(tierStatsInfo))
for _, stat := range tierStatsInfo {
tiersStatsMap[stat.Name] = stat.Stats
}
var tiersList []*models.Tier
for _, tierData := range tiers {
// Default Tier Stats
tierStats := madmin.TierStats{
NumObjects: 0,
NumVersions: 0,
TotalSize: 0,
}
if stats, ok := tiersStatsMap[tierData.Name]; ok {
tierStats = stats
}
status := client.verifyTierStatus(ctx, tierData.Name) == nil
switch tierData.Type {
case madmin.S3:
tiersList = append(tiersList, &models.Tier{
Type: models.TierTypeS3,
S3: &models.TierS3{
Accesskey: tierData.S3.AccessKey,
Bucket: tierData.S3.Bucket,
Endpoint: tierData.S3.Endpoint,
Name: tierData.Name,
Prefix: tierData.S3.Prefix,
Region: tierData.S3.Region,
Secretkey: tierData.S3.SecretKey,
Storageclass: tierData.S3.StorageClass,
Usage: humanize.IBytes(tierStats.TotalSize),
Objects: strconv.Itoa(tierStats.NumObjects),
Versions: strconv.Itoa(tierStats.NumVersions),
},
Status: status,
})
case madmin.MinIO:
tiersList = append(tiersList, &models.Tier{
Type: models.TierTypeMinio,
Minio: &models.TierMinio{
Accesskey: tierData.MinIO.AccessKey,
Bucket: tierData.MinIO.Bucket,
Endpoint: tierData.MinIO.Endpoint,
Name: tierData.Name,
Prefix: tierData.MinIO.Prefix,
Region: tierData.MinIO.Region,
Secretkey: tierData.MinIO.SecretKey,
Usage: humanize.IBytes(tierStats.TotalSize),
Objects: strconv.Itoa(tierStats.NumObjects),
Versions: strconv.Itoa(tierStats.NumVersions),
},
Status: status,
})
case madmin.GCS:
tiersList = append(tiersList, &models.Tier{
Type: models.TierTypeGcs,
Gcs: &models.TierGcs{
Bucket: tierData.GCS.Bucket,
Creds: tierData.GCS.Creds,
Endpoint: tierData.GCS.Endpoint,
Name: tierData.Name,
Prefix: tierData.GCS.Prefix,
Region: tierData.GCS.Region,
Usage: humanize.IBytes(tierStats.TotalSize),
Objects: strconv.Itoa(tierStats.NumObjects),
Versions: strconv.Itoa(tierStats.NumVersions),
},
Status: status,
})
case madmin.Azure:
tiersList = append(tiersList, &models.Tier{
Type: models.TierTypeAzure,
Azure: &models.TierAzure{
Accountkey: tierData.Azure.AccountKey,
Accountname: tierData.Azure.AccountName,
Bucket: tierData.Azure.Bucket,
Endpoint: tierData.Azure.Endpoint,
Name: tierData.Name,
Prefix: tierData.Azure.Prefix,
Region: tierData.Azure.Region,
Usage: humanize.IBytes(tierStats.TotalSize),
Objects: strconv.Itoa(tierStats.NumObjects),
Versions: strconv.Itoa(tierStats.NumVersions),
},
Status: status,
})
case madmin.Unsupported:
tiersList = append(tiersList, &models.Tier{
Type: models.TierTypeUnsupported,
Status: status,
})
}
}
// build response
return &models.TierListResponse{
Items: tiersList,
}, nil
}
// getTiersResponse returns a response with a list of tiers
func getTiersResponse(session *models.Principal, params tieringApi.TiersListParams) (*models.TierListResponse, *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}
// serialize output
tiersResp, err := getTiers(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return tiersResp, nil
}
// getTiersNameResponse returns a response with a list of tiers' names
func getTiersNameResponse(session *models.Principal, params tieringApi.TiersListNamesParams) (*models.TiersNameListResponse, *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}
// serialize output
tiersResp, err := getTiersName(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return tiersResp, nil
}
// getTiersName fetches listTiers and returns a list of the tiers' names
func getTiersName(ctx context.Context, client MinioAdmin) (*models.TiersNameListResponse, error) {
tiers, err := client.listTiers(ctx)
if err != nil {
return nil, err
}
tiersNameList := make([]string, len(tiers))
for i, tierData := range tiers {
tiersNameList[i] = tierData.Name
}
return &models.TiersNameListResponse{
Items: tiersNameList,
}, nil
}
func addTier(ctx context.Context, client MinioAdmin, params *tieringApi.AddTierParams) error {
var cfg *madmin.TierConfig
var err error
switch params.Body.Type {
case models.TierTypeS3:
cfg, err = madmin.NewTierS3(
params.Body.S3.Name,
params.Body.S3.Accesskey,
params.Body.S3.Secretkey,
params.Body.S3.Bucket,
madmin.S3Region(params.Body.S3.Region),
madmin.S3Prefix(params.Body.S3.Prefix),
madmin.S3Endpoint(params.Body.S3.Endpoint),
madmin.S3StorageClass(params.Body.S3.Storageclass),
)
if err != nil {
return err
}
case models.TierTypeMinio:
cfg, err = madmin.NewTierMinIO(
params.Body.Minio.Name,
params.Body.Minio.Endpoint,
params.Body.Minio.Accesskey,
params.Body.Minio.Secretkey,
params.Body.Minio.Bucket,
madmin.MinIORegion(params.Body.Minio.Region),
madmin.MinIOPrefix(params.Body.Minio.Prefix),
)
if err != nil {
return err
}
case models.TierTypeGcs:
gcsOpts := []madmin.GCSOptions{}
prefix := params.Body.Gcs.Prefix
if prefix != "" {
gcsOpts = append(gcsOpts, madmin.GCSPrefix(prefix))
}
region := params.Body.Gcs.Region
if region != "" {
gcsOpts = append(gcsOpts, madmin.GCSRegion(region))
}
base64Text := make([]byte, base64.StdEncoding.EncodedLen(len(params.Body.Gcs.Creds)))
l, _ := base64.StdEncoding.Decode(base64Text, []byte(params.Body.Gcs.Creds))
cfg, err = madmin.NewTierGCS(
params.Body.Gcs.Name,
base64Text[:l],
params.Body.Gcs.Bucket,
gcsOpts...,
)
if err != nil {
return err
}
case models.TierTypeAzure:
cfg, err = madmin.NewTierAzure(
params.Body.Azure.Name,
params.Body.Azure.Accountname,
params.Body.Azure.Accountkey,
params.Body.Azure.Bucket,
madmin.AzurePrefix(params.Body.Azure.Prefix),
madmin.AzureEndpoint(params.Body.Azure.Endpoint),
madmin.AzureRegion(params.Body.Azure.Region),
)
if err != nil {
return err
}
case models.TierTypeUnsupported:
cfg = &madmin.TierConfig{
Type: madmin.Unsupported,
}
}
err = client.addTier(ctx, cfg)
if err != nil {
return err
}
return nil
}
// getAddTierResponse returns the response of admin tier
func getAddTierResponse(session *models.Principal, params tieringApi.AddTierParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
// serialize output
errTier := addTier(ctx, adminClient, &params)
if errTier != nil {
return ErrorWithContext(ctx, errTier)
}
return nil
}
func getTier(ctx context.Context, client MinioAdmin, params *tieringApi.GetTierParams) (*models.Tier, error) {
tiers, err := client.listTiers(ctx)
if err != nil {
return nil, err
}
for i := range tiers {
switch tiers[i].Type {
case madmin.S3:
if params.Type != models.TierTypeS3 || tiers[i].Name != params.Name {
continue
}
return &models.Tier{
Type: models.TierTypeS3,
S3: &models.TierS3{
Accesskey: tiers[i].S3.AccessKey,
Bucket: tiers[i].S3.Bucket,
Endpoint: tiers[i].S3.Endpoint,
Name: tiers[i].Name,
Prefix: tiers[i].S3.Prefix,
Region: tiers[i].S3.Region,
Secretkey: tiers[i].S3.SecretKey,
Storageclass: tiers[i].S3.StorageClass,
},
}, err
case madmin.GCS:
if params.Type != models.TierTypeGcs || tiers[i].Name != params.Name {
continue
}
return &models.Tier{
Type: models.TierTypeGcs,
Gcs: &models.TierGcs{
Bucket: tiers[i].GCS.Bucket,
Creds: tiers[i].GCS.Creds,
Endpoint: tiers[i].GCS.Endpoint,
Name: tiers[i].Name,
Prefix: tiers[i].GCS.Prefix,
Region: tiers[i].GCS.Region,
},
}, nil
case madmin.Azure:
if params.Type != models.TierTypeAzure || tiers[i].Name != params.Name {
continue
}
return &models.Tier{
Type: models.TierTypeAzure,
Azure: &models.TierAzure{
Accountkey: tiers[i].Azure.AccountKey,
Accountname: tiers[i].Azure.AccountName,
Bucket: tiers[i].Azure.Bucket,
Endpoint: tiers[i].Azure.Endpoint,
Name: tiers[i].Name,
Prefix: tiers[i].Azure.Prefix,
Region: tiers[i].Azure.Region,
},
}, nil
}
}
// build response
return nil, ErrNotFound
}
// getGetTierResponse returns a tier
func getGetTierResponse(session *models.Principal, params tieringApi.GetTierParams) (*models.Tier, *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}
// serialize output
addTierResp, err := getTier(ctx, adminClient, &params)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return addTierResp, nil
}
func editTierCredentials(ctx context.Context, client MinioAdmin, params *tieringApi.EditTierCredentialsParams) error {
base64Text := make([]byte, base64.StdEncoding.EncodedLen(len(params.Body.Creds)))
l, err := base64.StdEncoding.Decode(base64Text, []byte(params.Body.Creds))
if err != nil {
return err
}
creds := madmin.TierCreds{
AccessKey: params.Body.AccessKey,
SecretKey: params.Body.SecretKey,
CredsJSON: base64Text[:l],
}
return client.editTierCreds(ctx, params.Name, creds)
}
// getEditTierCredentialsResponse returns the result of editing credentials for a tier
func getEditTierCredentialsResponse(session *models.Principal, params tieringApi.EditTierCredentialsParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
// serialize output
err = editTierCredentials(ctx, adminClient, &params)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func removeTier(ctx context.Context, client MinioAdmin, params *tieringApi.RemoveTierParams) error {
return client.removeTier(ctx, params.Name)
}
func getRemoveTierResponse(session *models.Principal, params tieringApi.RemoveTierParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
// serialize output
err = removeTier(ctx, adminClient, &params)
if err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}

View File

@@ -1,310 +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"
"testing"
tieringApi "github.com/minio/console/api/operations/tiering"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
"github.com/stretchr/testify/assert"
)
func TestGetTiers(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "getTiers()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Test-1 : getTiers() get list of tiers
// mock lifecycle response from MinIO
returnListMock := []*madmin.TierConfig{
{
Version: "V1",
Type: madmin.S3,
Name: "S3 Tier",
S3: &madmin.TierS3{
Endpoint: "https://s3tier.test.com/",
AccessKey: "Access Key",
SecretKey: "Secret Key",
Bucket: "buckets3",
Prefix: "pref1",
Region: "us-west-1",
StorageClass: "TT1",
},
},
{
Version: "V1",
Type: madmin.MinIO,
Name: "MinIO Tier",
MinIO: &madmin.TierMinIO{
Endpoint: "https://minio-endpoint.test.com/",
AccessKey: "access",
SecretKey: "secret",
Bucket: "somebucket",
Prefix: "p1",
Region: "us-east-2",
},
},
}
returnStatsMock := []madmin.TierInfo{
{
Name: "STANDARD",
Type: "internal",
Stats: madmin.TierStats{NumObjects: 2, NumVersions: 2, TotalSize: 228915},
},
{
Name: "MinIO Tier",
Type: "internal",
Stats: madmin.TierStats{NumObjects: 10, NumVersions: 3, TotalSize: 132788},
},
{
Name: "S3 Tier",
Type: "s3",
Stats: madmin.TierStats{NumObjects: 0, NumVersions: 0, TotalSize: 0},
},
}
expectedOutput := &models.TierListResponse{
Items: []*models.Tier{
{
Type: models.TierTypeS3,
S3: &models.TierS3{
Accesskey: "Access Key",
Secretkey: "Secret Key",
Bucket: "buckets3",
Endpoint: "https://s3tier.test.com/",
Name: "S3 Tier",
Prefix: "pref1",
Region: "us-west-1",
Storageclass: "TT1",
Usage: "0 B",
Objects: "0",
Versions: "0",
},
Status: false,
},
{
Type: models.TierTypeMinio,
Minio: &models.TierMinio{
Accesskey: "access",
Secretkey: "secret",
Bucket: "somebucket",
Endpoint: "https://minio-endpoint.test.com/",
Name: "MinIO Tier",
Prefix: "p1",
Region: "us-east-2",
Usage: "130 KiB",
Objects: "10",
Versions: "3",
},
Status: false,
},
},
}
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
return returnListMock, nil
}
minioTierStatsMock = func(_ context.Context) ([]madmin.TierInfo, error) {
return returnStatsMock, nil
}
minioVerifyTierStatusMock = func(_ context.Context, _ string) error {
return fmt.Errorf("someerror")
}
tiersList, err := getTiers(ctx, adminClient)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// verify length of tiers list is correct
assert.Equal(len(tiersList.Items), len(returnListMock), fmt.Sprintf("Failed on %s: length of lists is not the same", function))
assert.Equal(expectedOutput, tiersList)
// Test-2 : getTiers() list is empty
returnListMockT2 := []*madmin.TierConfig{}
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
return returnListMockT2, nil
}
tiersListT2, err := getTiers(ctx, adminClient)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
if len(tiersListT2.Items) != 0 {
t.Errorf("Failed on %s:, returned list was not empty", function)
}
}
func TestGetTiersName(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "getTiersName()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Test-1 : getTiersName() get list tiers' names
// mock lifecycle response from MinIO
returnListMock := []*madmin.TierConfig{
{
Version: "V1",
Type: madmin.S3,
Name: "S3 Tier",
S3: &madmin.TierS3{
Endpoint: "https://s3tier.test.com/",
AccessKey: "Access Key",
SecretKey: "Secret Key",
Bucket: "buckets3",
Prefix: "pref1",
Region: "us-west-1",
StorageClass: "TT1",
},
},
{
Version: "V1",
Type: madmin.MinIO,
Name: "MinIO Tier",
MinIO: &madmin.TierMinIO{
Endpoint: "https://minio-endpoint.test.com/",
AccessKey: "access",
SecretKey: "secret",
Bucket: "somebucket",
Prefix: "p1",
Region: "us-east-2",
},
},
}
expectedOutput := &models.TiersNameListResponse{
Items: []string{"S3 Tier", "MinIO Tier"},
}
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
return returnListMock, nil
}
tiersList, err := getTiersName(ctx, adminClient)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// verify length of tiers list is correct
assert.Equal(len(tiersList.Items), len(returnListMock), fmt.Sprintf("Failed on %s: length of lists is not the same", function))
assert.Equal(expectedOutput, tiersList)
// Test-2 : getTiersName() list is empty
returnListMockT2 := []*madmin.TierConfig{}
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
return returnListMockT2, nil
}
emptyTierList, err := getTiersName(ctx, adminClient)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
if len(emptyTierList.Items) != 0 {
t.Errorf("Failed on %s:, returned list was not empty", function)
}
}
func TestAddTier(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "addTier()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Test-1: addTier() add new Tier
minioAddTiersMock = func(_ context.Context, _ *madmin.TierConfig) error {
return nil
}
paramsToAdd := tieringApi.AddTierParams{
Body: &models.Tier{
Type: "S3",
S3: &models.TierS3{
Accesskey: "TestAK",
Bucket: "bucket1",
Endpoint: "https://test.com/",
Name: "TIERS3",
Prefix: "Pr1",
Region: "us-west-1",
Secretkey: "SecretK",
Storageclass: "STCLASS",
},
},
}
err := addTier(ctx, adminClient, &paramsToAdd)
assert.Equal(nil, err, fmt.Sprintf("Failed on %s: Error returned", function))
// Test-2: addTier() error adding Tier
minioAddTiersMock = func(_ context.Context, _ *madmin.TierConfig) error {
return errors.New("error setting new tier")
}
err2 := addTier(ctx, adminClient, &paramsToAdd)
assert.Equal(errors.New("error setting new tier"), err2, fmt.Sprintf("Failed on %s: Error returned", function))
}
func TestUpdateTierCreds(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := AdminClientMock{}
function := "editTierCredentials()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Test-1: editTierCredentials() update Tier configuration
minioEditTiersMock = func(_ context.Context, _ string, _ madmin.TierCreds) error {
return nil
}
params := &tieringApi.EditTierCredentialsParams{
Name: "TESTTIER",
Body: &models.TierCredentialsRequest{
AccessKey: "New Key",
SecretKey: "Secret Key",
},
}
err := editTierCredentials(ctx, adminClient, params)
assert.Equal(nil, err, fmt.Sprintf("Failed on %s: Error returned", function))
// Test-2: editTierCredentials() update Tier configuration failure
minioEditTiersMock = func(_ context.Context, _ string, _ madmin.TierCreds) error {
return errors.New("error message")
}
errT2 := editTierCredentials(ctx, adminClient, params)
assert.Equal(errors.New("error message"), errT2, fmt.Sprintf("Failed on %s: Error returned", function))
}

View File

@@ -1,154 +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"
"net/http"
"strings"
"time"
"github.com/minio/madmin-go/v3"
"github.com/minio/websocket"
)
// shortTraceMsg Short trace record
type shortTraceMsg struct {
Host string `json:"host"`
Time string `json:"time"`
Client string `json:"client"`
CallStats callStats `json:"callStats"`
FuncName string `json:"api"`
Path string `json:"path"`
Query string `json:"query"`
StatusCode int `json:"statusCode"`
StatusMsg string `json:"statusMsg"`
}
type callStats struct {
Rx int `json:"rx"`
Tx int `json:"tx"`
Duration string `json:"duration"`
Ttfb string `json:"timeToFirstByte"`
}
// trace filters
func matchTrace(opts TraceRequest, traceInfo madmin.ServiceTraceInfo) bool {
statusCode := int(opts.statusCode)
method := opts.method
funcName := opts.funcName
apiPath := opts.path
if statusCode == 0 && method == "" && funcName == "" && apiPath == "" {
// no specific filtering found trace all the requests
return true
}
// Filter request path if passed by the user
if apiPath != "" {
pathToLookup := strings.ToLower(apiPath)
pathFromTrace := strings.ToLower(traceInfo.Trace.Path)
return strings.Contains(pathFromTrace, pathToLookup)
}
// Filter response status codes if passed by the user
if statusCode > 0 && traceInfo.Trace.HTTP != nil {
statusCodeFromTrace := traceInfo.Trace.HTTP.RespInfo.StatusCode
return statusCodeFromTrace == statusCode
}
// Filter request method if passed by the user
if method != "" && traceInfo.Trace.HTTP != nil {
methodFromTrace := traceInfo.Trace.HTTP.ReqInfo.Method
return methodFromTrace == method
}
if funcName != "" {
funcToLookup := strings.ToLower(funcName)
funcFromTrace := strings.ToLower(traceInfo.Trace.FuncName)
return strings.Contains(funcFromTrace, funcToLookup)
}
return true
}
// startTraceInfo starts trace of the servers
func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin, opts TraceRequest) error {
// Start listening on all trace activity.
traceCh := client.serviceTrace(ctx, opts.threshold, opts.s3, opts.internal, opts.storage, opts.os, opts.onlyErrors)
for {
select {
case <-ctx.Done():
return nil
case traceInfo, ok := <-traceCh:
// zero value returned because the channel is closed and empty
if !ok {
return nil
}
if traceInfo.Err != nil {
LogError("error on serviceTrace: %v", traceInfo.Err)
return traceInfo.Err
}
if matchTrace(opts, traceInfo) {
// Serialize message to be sent
traceInfoBytes, err := json.Marshal(shortTrace(&traceInfo))
if err != nil {
LogError("error on json.Marshal: %v", err)
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, traceInfoBytes)
if err != nil {
LogError("error writeMessage: %v", err)
return err
}
}
}
}
}
// shortTrace creates a shorter Trace Info message.
// Same implementation as github/minio/mc/cmd/admin-trace.go
func shortTrace(info *madmin.ServiceTraceInfo) shortTraceMsg {
t := info.Trace
s := shortTraceMsg{}
s.Time = t.Time.Format(time.RFC3339)
s.Path = t.Path
s.FuncName = t.FuncName
s.CallStats.Duration = t.Duration.String()
if info.Trace.HTTP != nil {
s.Query = t.HTTP.ReqInfo.RawQuery
s.StatusCode = t.HTTP.RespInfo.StatusCode
s.StatusMsg = http.StatusText(t.HTTP.RespInfo.StatusCode)
s.CallStats.Rx = t.HTTP.CallStats.InputBytes
s.CallStats.Tx = t.HTTP.CallStats.OutputBytes
s.CallStats.Ttfb = t.HTTP.CallStats.TimeToFirstByte.String()
if host, ok := t.HTTP.ReqInfo.Headers["Host"]; ok {
s.Host = strings.Join(host, "")
}
cSlice := strings.Split(t.HTTP.ReqInfo.Client, ":")
s.Client = cSlice[0]
}
return s
}

View File

@@ -1,722 +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"
"sort"
"strings"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/api/operations"
accountApi "github.com/minio/console/api/operations/account"
bucketApi "github.com/minio/console/api/operations/bucket"
userApi "github.com/minio/console/api/operations/user"
"github.com/minio/console/models"
"github.com/minio/madmin-go/v3"
iampolicy "github.com/minio/pkg/v3/policy"
)
// Policy evaluated constants
const (
Unknown = 0
Allow = 1
Deny = -1
)
func registerUsersHandlers(api *operations.ConsoleAPI) {
// List Users
api.UserListUsersHandler = userApi.ListUsersHandlerFunc(func(params userApi.ListUsersParams, session *models.Principal) middleware.Responder {
listUsersResponse, err := getListUsersResponse(session, params)
if err != nil {
return userApi.NewListUsersDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewListUsersOK().WithPayload(listUsersResponse)
})
// Add User
api.UserAddUserHandler = userApi.AddUserHandlerFunc(func(params userApi.AddUserParams, session *models.Principal) middleware.Responder {
userResponse, err := getUserAddResponse(session, params)
if err != nil {
return userApi.NewAddUserDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewAddUserCreated().WithPayload(userResponse)
})
// Remove User
api.UserRemoveUserHandler = userApi.RemoveUserHandlerFunc(func(params userApi.RemoveUserParams, session *models.Principal) middleware.Responder {
err := getRemoveUserResponse(session, params)
if err != nil {
return userApi.NewRemoveUserDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewRemoveUserNoContent()
})
// Update User-Groups
api.UserUpdateUserGroupsHandler = userApi.UpdateUserGroupsHandlerFunc(func(params userApi.UpdateUserGroupsParams, session *models.Principal) middleware.Responder {
userUpdateResponse, err := getUpdateUserGroupsResponse(session, params)
if err != nil {
return userApi.NewUpdateUserGroupsDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewUpdateUserGroupsOK().WithPayload(userUpdateResponse)
})
// Get User
api.UserGetUserInfoHandler = userApi.GetUserInfoHandlerFunc(func(params userApi.GetUserInfoParams, session *models.Principal) middleware.Responder {
userInfoResponse, err := getUserInfoResponse(session, params)
if err != nil {
return userApi.NewGetUserInfoDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewGetUserInfoOK().WithPayload(userInfoResponse)
})
// Update User
api.UserUpdateUserInfoHandler = userApi.UpdateUserInfoHandlerFunc(func(params userApi.UpdateUserInfoParams, session *models.Principal) middleware.Responder {
userUpdateResponse, err := getUpdateUserResponse(session, params)
if err != nil {
return userApi.NewUpdateUserInfoDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewUpdateUserInfoOK().WithPayload(userUpdateResponse)
})
// Update User-Groups Bulk
api.UserBulkUpdateUsersGroupsHandler = userApi.BulkUpdateUsersGroupsHandlerFunc(func(params userApi.BulkUpdateUsersGroupsParams, session *models.Principal) middleware.Responder {
err := getAddUsersListToGroupsResponse(session, params)
if err != nil {
return userApi.NewBulkUpdateUsersGroupsDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewBulkUpdateUsersGroupsOK()
})
api.BucketListUsersWithAccessToBucketHandler = bucketApi.ListUsersWithAccessToBucketHandlerFunc(func(params bucketApi.ListUsersWithAccessToBucketParams, session *models.Principal) middleware.Responder {
response, err := getListUsersWithAccessToBucketResponse(session, params)
if err != nil {
return bucketApi.NewListUsersWithAccessToBucketDefault(err.Code).WithPayload(err.APIError)
}
return bucketApi.NewListUsersWithAccessToBucketOK().WithPayload(response)
})
// Change User Password
api.AccountChangeUserPasswordHandler = accountApi.ChangeUserPasswordHandlerFunc(func(params accountApi.ChangeUserPasswordParams, session *models.Principal) middleware.Responder {
err := getChangeUserPasswordResponse(session, params)
if err != nil {
return accountApi.NewChangeUserPasswordDefault(err.Code).WithPayload(err.APIError)
}
return accountApi.NewChangeUserPasswordCreated()
})
// Check number of Service Accounts for listed users
api.UserCheckUserServiceAccountsHandler = userApi.CheckUserServiceAccountsHandlerFunc(func(params userApi.CheckUserServiceAccountsParams, session *models.Principal) middleware.Responder {
userSAList, err := getCheckUserSAResponse(session, params)
if err != nil {
return userApi.NewCheckUserServiceAccountsDefault(err.Code).WithPayload(err.APIError)
}
return userApi.NewCheckUserServiceAccountsOK().WithPayload(userSAList)
})
}
func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
// Get list of all users in the MinIO
// This call requires explicit authentication, no anonymous requests are
// allowed for listing users.
userMap, err := client.listUsers(ctx)
if err != nil {
return []*models.User{}, err
}
var users []*models.User
for accessKey, user := range userMap {
userElem := &models.User{
AccessKey: accessKey,
Status: string(user.Status),
Policy: strings.Split(user.PolicyName, ","),
MemberOf: user.MemberOf,
}
users = append(users, userElem)
}
return users, nil
}
// getListUsersResponse performs listUsers() and serializes it to the handler's output
func getListUsersResponse(session *models.Principal, params userApi.ListUsersParams) (*models.ListUsersResponse, *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}
users, err := listUsers(ctx, adminClient)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// serialize output
listUsersResponse := &models.ListUsersResponse{
Users: users,
}
return listUsersResponse, nil
}
// addUser invokes adding a users on `MinioAdmin` and builds the response `models.User`
func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *string, groups []string, policies []string) (*models.User, error) {
// Calls into MinIO to add a new user if there's an errors return it
if err := client.addUser(ctx, *accessKey, *secretKey); err != nil {
return nil, err
}
// set groups for the newly created user
var userWithGroups *models.User
if len(groups) > 0 {
var errUG error
userWithGroups, errUG = updateUserGroups(ctx, client, *accessKey, groups)
if errUG != nil {
return nil, errUG
}
}
// set policies for the newly created user
if len(policies) > 0 {
policyString := strings.Join(policies, ",")
if err := SetPolicy(ctx, client, policyString, *accessKey, "user"); err != nil {
return nil, err
}
}
memberOf := []string{}
status := "enabled"
if userWithGroups != nil {
memberOf = userWithGroups.MemberOf
status = userWithGroups.Status
}
userRet := &models.User{
AccessKey: *accessKey,
MemberOf: memberOf,
Policy: policies,
Status: status,
}
return userRet, nil
}
func getUserAddResponse(session *models.Principal, params userApi.AddUserParams) (*models.User, *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}
var userExists bool
_, err = adminClient.getUserInfo(ctx, *params.Body.AccessKey)
userExists = err == nil
if userExists {
return nil, ErrorWithContext(ctx, ErrNonUniqueAccessKey)
}
user, err := addUser(
ctx,
adminClient,
params.Body.AccessKey,
params.Body.SecretKey,
params.Body.Groups,
params.Body.Policies,
)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return user, nil
}
// removeUser invokes removing an user on `MinioAdmin`, then we return the response from API
func removeUser(ctx context.Context, client MinioAdmin, accessKey string) error {
return client.removeUser(ctx, accessKey)
}
func getRemoveUserResponse(session *models.Principal, params userApi.RemoveUserParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
if session.AccountAccessKey == params.Name {
return ErrorWithContext(ctx, ErrAvoidSelfAccountDelete)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
if err := removeUser(ctx, adminClient, params.Name); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
// getUserInfo calls MinIO server get the User Information
func getUserInfo(ctx context.Context, client MinioAdmin, accessKey string) (*madmin.UserInfo, error) {
userInfo, err := client.getUserInfo(ctx, accessKey)
if err != nil {
return nil, err
}
return &userInfo, nil
}
func getUserInfoResponse(session *models.Principal, params userApi.GetUserInfoParams) (*models.User, *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}
user, err := getUserInfo(ctx, adminClient, params.Name)
if err != nil {
// User doesn't exist, return 404
if madmin.ToErrorResponse(err).Code == "XMinioAdminNoSuchUser" {
errorCode := 404
errorMessage := "User doesn't exist"
return nil, &CodedAPIError{Code: errorCode, APIError: &models.APIError{Message: errorMessage, DetailedMessage: err.Error()}}
}
return nil, ErrorWithContext(ctx, err)
}
var policies []string
if user.PolicyName == "" {
policies = []string{}
} else {
policies = strings.Split(user.PolicyName, ",")
}
hasPolicy := true
if len(policies) == 0 {
hasPolicy = false
for i := 0; i < len(user.MemberOf); i++ {
group, err := adminClient.getGroupDescription(ctx, user.MemberOf[i])
if err != nil {
continue
}
if group.Policy != "" {
hasPolicy = true
break
}
}
}
userInformation := &models.User{
AccessKey: params.Name,
MemberOf: user.MemberOf,
Policy: policies,
Status: string(user.Status),
HasPolicy: hasPolicy,
}
return userInformation, nil
}
// updateUserGroups invokes getUserInfo() to get the old groups from the user,
// then we merge the list with the new groups list to have a shorter iteration between groups and we do a comparison between the current and old groups.
// We delete or update the groups according the location in each list and send the user with the new groups from `MinioAdmin` to the client
func updateUserGroups(ctx context.Context, client MinioAdmin, user string, groupsToAssign []string) (*models.User, error) {
parallelUserUpdate := func(groupName string, originGroups []string) chan error {
chProcess := make(chan error)
go func() error {
defer close(chProcess)
// Compare if groupName is in the arrays
isGroupPersistent := IsElementInArray(groupsToAssign, groupName)
isInOriginGroups := IsElementInArray(originGroups, groupName)
if isGroupPersistent && isInOriginGroups { // Group is already assigned and doesn't need to be updated
chProcess <- nil
return nil
}
isRemove := false // User is added by default
// User is deleted from the group
if !isGroupPersistent {
isRemove = true
}
userToAddRemove := []string{user}
updateReturn := updateGroupMembers(ctx, client, groupName, userToAddRemove, isRemove)
chProcess <- updateReturn
return updateReturn
}()
return chProcess
}
userInfoOr, err := getUserInfo(ctx, client, user)
if err != nil {
return nil, err
}
memberOf := userInfoOr.MemberOf
mergedGroupArray := UniqueKeys(append(memberOf, groupsToAssign...))
var listOfUpdates []chan error
// Each group must be updated individually because there is no way to update all the groups at once for a user,
// we are using the same logic as 'mc admin group add' command
for _, groupN := range mergedGroupArray {
proc := parallelUserUpdate(groupN, memberOf)
listOfUpdates = append(listOfUpdates, proc)
}
channelHasError := false
for _, chanRet := range listOfUpdates {
locError := <-chanRet
if locError != nil {
channelHasError = true
}
}
if channelHasError {
errRt := errors.New(500, "there was an error updating the groups")
return nil, errRt
}
userInfo, err := getUserInfo(ctx, client, user)
if err != nil {
return nil, err
}
policies := strings.Split(userInfo.PolicyName, ",")
userReturn := &models.User{
AccessKey: user,
MemberOf: userInfo.MemberOf,
Policy: policies,
Status: string(userInfo.Status),
}
return userReturn, nil
}
func getUpdateUserGroupsResponse(session *models.Principal, params userApi.UpdateUserGroupsParams) (*models.User, *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}
user, err := updateUserGroups(ctx, adminClient, params.Name, params.Body.Groups)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return user, nil
}
// setUserStatus invokes setUserStatus from madmin to update user status
func setUserStatus(ctx context.Context, client MinioAdmin, user string, status string) error {
var setStatus madmin.AccountStatus
switch status {
case "enabled":
setStatus = madmin.AccountEnabled
case "disabled":
setStatus = madmin.AccountDisabled
default:
return errors.New(500, "status not valid")
}
return client.setUserStatus(ctx, user, setStatus)
}
func getUpdateUserResponse(session *models.Principal, params userApi.UpdateUserInfoParams) (*models.User, *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}
status := *params.Body.Status
groups := params.Body.Groups
if err := setUserStatus(ctx, adminClient, params.Name, status); err != nil {
return nil, ErrorWithContext(ctx, err)
}
userElem, errUG := updateUserGroups(ctx, adminClient, params.Name, groups)
if errUG != nil {
return nil, ErrorWithContext(ctx, errUG)
}
return userElem, nil
}
// addUsersListToGroups iterates over the user list & assigns the requested groups to each user.
func addUsersListToGroups(ctx context.Context, client MinioAdmin, usersToUpdate []string, groupsToAssign []string) error {
// We update each group with the complete usersList
parallelGroupsUpdate := func(groupToAssign string) chan error {
groupProcess := make(chan error)
go func() {
defer close(groupProcess)
// We add the users array to the group.
err := updateGroupMembers(ctx, client, groupToAssign, usersToUpdate, false)
groupProcess <- err
}()
return groupProcess
}
var groupsUpdateList []chan error
// We get each group name & add users accordingly
for _, groupName := range groupsToAssign {
// We update the group
proc := parallelGroupsUpdate(groupName)
groupsUpdateList = append(groupsUpdateList, proc)
}
errorsList := []string{} // We get the errors list because we want to have all errors at once.
for _, err := range groupsUpdateList {
errorFromUpdate := <-err // We store the errors to avoid Data Race
if errorFromUpdate != nil {
// If there is an errors, we store the errors strings so we can join them after we receive all errors
errorsList = append(errorsList, errorFromUpdate.Error()) // We wait until all the channels have been closed.
}
}
// If there are errors, we throw the final errors with the errors inside
if len(errorsList) > 0 {
errGen := fmt.Errorf("error in users-groups assignation: %q", strings.Join(errorsList, ","))
return errGen
}
return nil
}
func getAddUsersListToGroupsResponse(session *models.Principal, params userApi.BulkUpdateUsersGroupsParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
usersList := params.Body.Users
groupsList := params.Body.Groups
if err := addUsersListToGroups(ctx, adminClient, usersList, groupsList); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func getListUsersWithAccessToBucketResponse(session *models.Principal, params bucketApi.ListUsersWithAccessToBucketParams) ([]string, *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}
list, err := listUsersWithAccessToBucket(ctx, adminClient, params.Bucket)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
return list, nil
}
func policyAllowsAndMatchesBucket(policy *iampolicy.Policy, bucket string) int {
policyStatements := policy.Statements
for i := 0; i < len(policyStatements); i++ {
resources := policyStatements[i].Resources
effect := policyStatements[i].Effect
if resources.Match(bucket, map[string][]string{}) {
if effect.IsValid() {
if effect.IsAllowed(true) {
return Allow
}
return Deny
}
}
}
return Unknown
}
func listUsersWithAccessToBucket(ctx context.Context, adminClient MinioAdmin, bucket string) ([]string, error) {
users, err := adminClient.listUsers(ctx)
if err != nil {
return nil, err
}
var retval []string
akHasAccess := make(map[string]struct{})
akIsDenied := make(map[string]struct{})
for k, v := range users {
for _, policyName := range strings.Split(v.PolicyName, ",") {
policyName = strings.TrimSpace(policyName)
if policyName == "" {
continue
}
policy, err := adminClient.getPolicy(ctx, policyName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("unable to fetch policy %s: %v", policyName, err))
continue
}
if _, ok := akIsDenied[k]; !ok {
switch policyAllowsAndMatchesBucket(policy, bucket) {
case Allow:
if _, ok := akHasAccess[k]; !ok {
akHasAccess[k] = struct{}{}
}
case Deny:
akIsDenied[k] = struct{}{}
delete(akHasAccess, k)
}
}
}
}
groups, err := adminClient.listGroups(ctx)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("unable to list groups: %v", err))
return retval, nil
}
for _, groupName := range groups {
info, err := groupInfo(ctx, adminClient, groupName)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("unable to fetch group info %s: %v", groupName, err))
continue
}
policy, err := adminClient.getPolicy(ctx, info.Policy)
if err != nil {
ErrorWithContext(ctx, fmt.Errorf("unable to fetch group policy %s: %v", info.Policy, err))
continue
}
for _, member := range info.Members {
if _, ok := akIsDenied[member]; !ok {
switch policyAllowsAndMatchesBucket(policy, bucket) {
case Allow:
if _, ok := akHasAccess[member]; !ok {
akHasAccess[member] = struct{}{}
}
case Deny:
akIsDenied[member] = struct{}{}
delete(akHasAccess, member)
}
}
}
}
for k := range akHasAccess {
retval = append(retval, k)
}
sort.Strings(retval)
return retval, nil
}
// changeUserPassword changes password of selectedUser to newSecretKey
func changeUserPassword(ctx context.Context, client MinioAdmin, selectedUser string, newSecretKey string) error {
return client.changePassword(ctx, selectedUser, newSecretKey)
}
// getChangeUserPasswordResponse will change the password of selctedUser to newSecretKey
func getChangeUserPasswordResponse(session *models.Principal, params accountApi.ChangeUserPasswordParams) *CodedAPIError {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
if err != nil {
return ErrorWithContext(ctx, err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
// params will contain selectedUser and newSecretKey credentials for the user
user := *params.Body.SelectedUser
newSecretKey := *params.Body.NewSecretKey
// changes password of user to newSecretKey
if err := changeUserPassword(ctx, adminClient, user, newSecretKey); err != nil {
return ErrorWithContext(ctx, err)
}
return nil
}
func getCheckUserSAResponse(session *models.Principal, params userApi.CheckUserServiceAccountsParams) (*models.UserServiceAccountSummary, *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}
var userServiceAccountList []*models.UserServiceAccountItem
hasSA := false
for _, user := range params.SelectedUsers {
listServAccs, err := adminClient.listServiceAccounts(ctx, user)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
numSAs := int64(len(listServAccs.Accounts))
if numSAs > 0 {
hasSA = true
}
userAccountItem := &models.UserServiceAccountItem{
UserName: user,
NumSAs: numSAs,
}
userServiceAccountList = append(userServiceAccountList, userAccountItem)
}
userAccountList := &models.UserServiceAccountSummary{
UserServiceAccountList: userServiceAccountList,
HasSA: hasSA,
}
return userAccountList, nil
}

View File

@@ -1,692 +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"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"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"
iampolicy "github.com/minio/pkg/v3/policy"
)
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 {
listUsers(ctx context.Context) (map[string]madmin.UserInfo, error)
addUser(ctx context.Context, acessKey, SecretKey string) error
removeUser(ctx context.Context, accessKey string) error
getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error)
setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error
listGroups(ctx context.Context) ([]string, error)
updateGroupMembers(ctx context.Context, greq madmin.GroupAddRemove) error
getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error)
setGroupStatus(ctx context.Context, group string, status madmin.GroupStatus) error
listPolicies(ctx context.Context) (map[string]*iampolicy.Policy, error)
getPolicy(ctx context.Context, name string) (*iampolicy.Policy, error)
removePolicy(ctx context.Context, name string) error
addPolicy(ctx context.Context, name string, policy *iampolicy.Policy) error
setPolicy(ctx context.Context, policyName, entityName string, isGroup bool) error
getConfigKV(ctx context.Context, key string) ([]byte, error)
helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error)
helpConfigKVGlobal(ctx context.Context, envOnly bool) (madmin.Help, error)
setConfigKV(ctx context.Context, kv string) (restart bool, err error)
delConfigKV(ctx context.Context, kv string) (err error)
serviceRestart(ctx context.Context) error
serverInfo(ctx context.Context) (madmin.InfoMessage, error)
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
stopProfiling(ctx context.Context) (io.ReadCloser, error)
serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
AccountInfo(ctx context.Context) (madmin.AccountInfo, error)
heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
// Service Accounts
addServiceAccount(ctx context.Context, policy string, user string, accessKey string, secretKey string, name string, description string, expiry *time.Time, comment string) (madmin.Credentials, error)
listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
deleteServiceAccount(ctx context.Context, serviceAccount string) error
infoServiceAccount(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error)
updateServiceAccount(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error
// Remote Buckets
listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error)
getRemoteBucket(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error)
removeRemoteBucket(ctx context.Context, bucket, arn string) error
addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error)
// Account password management
changePassword(ctx context.Context, accessKey, secretKey string) error
serverHealthInfo(ctx context.Context, deadline time.Duration) (interface{}, string, error)
// List Tiers
listTiers(ctx context.Context) ([]*madmin.TierConfig, error)
// Tier Info
tierStats(ctx context.Context) ([]madmin.TierInfo, error)
// Add Tier
addTier(ctx context.Context, tier *madmin.TierConfig) error
// Edit Tier Credentials
editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error
// verify Tier status
verifyTierStatus(ctx context.Context, tierName string) error
// remove empty Tier
removeTier(ctx context.Context, tierName string) error
// Speedtest
speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error)
// Site Relication
getSiteReplicationInfo(ctx context.Context) (*madmin.SiteReplicationInfo, error)
addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite, opts madmin.SRAddOptions) (*madmin.ReplicateAddStatus, error)
editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo, opts madmin.SREditOptions) (*madmin.ReplicateEditStatus, error)
deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error)
// Replication status
getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error)
// KMS
kmsStatus(ctx context.Context) (madmin.KMSStatus, error)
kmsMetrics(ctx context.Context) (*madmin.KMSMetrics, error)
kmsAPIs(ctx context.Context) ([]madmin.KMSAPI, error)
kmsVersion(ctx context.Context) (*madmin.KMSVersion, error)
createKey(ctx context.Context, key string) error
listKeys(ctx context.Context, pattern string) ([]madmin.KMSKeyInfo, error)
keyStatus(ctx context.Context, key string) (*madmin.KMSKeyStatus, error)
// IDP
addOrUpdateIDPConfig(ctx context.Context, idpType, cfgName, cfgData string, update bool) (restart bool, err error)
listIDPConfig(ctx context.Context, idpType string) ([]madmin.IDPListItem, error)
deleteIDPConfig(ctx context.Context, idpType, cfgName string) (restart bool, err error)
getIDPConfig(ctx context.Context, cfgType, cfgName string) (c madmin.IDPConfig, err error)
// LDAP
getLDAPPolicyEntities(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, 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
}
func (ac AdminClient) changePassword(ctx context.Context, accessKey, secretKey string) error {
return ac.Client.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
}
// implements madmin.ListUsers()
func (ac AdminClient) listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
return ac.Client.ListUsers(ctx)
}
// implements madmin.AddUser()
func (ac AdminClient) addUser(ctx context.Context, accessKey, secretKey string) error {
return ac.Client.AddUser(ctx, accessKey, secretKey)
}
// implements madmin.RemoveUser()
func (ac AdminClient) removeUser(ctx context.Context, accessKey string) error {
return ac.Client.RemoveUser(ctx, accessKey)
}
// implements madmin.GetUserInfo()
func (ac AdminClient) getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error) {
return ac.Client.GetUserInfo(ctx, accessKey)
}
// implements madmin.SetUserStatus()
func (ac AdminClient) setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error {
return ac.Client.SetUserStatus(ctx, accessKey, status)
}
// implements madmin.ListGroups()
func (ac AdminClient) listGroups(ctx context.Context) ([]string, error) {
return ac.Client.ListGroups(ctx)
}
// implements madmin.UpdateGroupMembers()
func (ac AdminClient) updateGroupMembers(ctx context.Context, greq madmin.GroupAddRemove) error {
return ac.Client.UpdateGroupMembers(ctx, greq)
}
// implements madmin.GetGroupDescription(group)
func (ac AdminClient) getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error) {
return ac.Client.GetGroupDescription(ctx, group)
}
// implements madmin.SetGroupStatus(group, status)
func (ac AdminClient) setGroupStatus(ctx context.Context, group string, status madmin.GroupStatus) error {
return ac.Client.SetGroupStatus(ctx, group, status)
}
// implements madmin.ListCannedPolicies()
func (ac AdminClient) listPolicies(ctx context.Context) (map[string]*iampolicy.Policy, error) {
policyMap, err := ac.Client.ListCannedPolicies(ctx)
if err != nil {
return nil, err
}
policies := make(map[string]*iampolicy.Policy, len(policyMap))
for k, v := range policyMap {
p, err := iampolicy.ParseConfig(bytes.NewReader(v))
if err != nil {
return nil, err
}
policies[k] = p
}
return policies, nil
}
// implements madmin.ListCannedPolicies()
func (ac AdminClient) getPolicy(ctx context.Context, name string) (*iampolicy.Policy, error) {
info, err := ac.Client.InfoCannedPolicyV2(ctx, name)
if err != nil {
return nil, err
}
return iampolicy.ParseConfig(bytes.NewReader(info.Policy))
}
// implements madmin.RemoveCannedPolicy()
func (ac AdminClient) removePolicy(ctx context.Context, name string) error {
return ac.Client.RemoveCannedPolicy(ctx, name)
}
// implements madmin.AddCannedPolicy()
func (ac AdminClient) addPolicy(ctx context.Context, name string, policy *iampolicy.Policy) error {
buf, err := json.Marshal(policy)
if err != nil {
return err
}
return ac.Client.AddCannedPolicy(ctx, name, buf)
}
// implements madmin.SetPolicy()
func (ac AdminClient) setPolicy(ctx context.Context, policyName, entityName string, isGroup bool) error {
// nolint:staticcheck // ignore SA1019
return ac.Client.SetPolicy(ctx, policyName, entityName, isGroup)
}
// implements madmin.GetConfigKV()
func (ac AdminClient) getConfigKV(ctx context.Context, key string) ([]byte, error) {
return ac.Client.GetConfigKV(ctx, key)
}
// implements madmin.HelpConfigKV()
func (ac AdminClient) helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error) {
return ac.Client.HelpConfigKV(ctx, subSys, key, envOnly)
}
// implements madmin.helpConfigKVGlobal()
func (ac AdminClient) helpConfigKVGlobal(ctx context.Context, envOnly bool) (madmin.Help, error) {
return ac.Client.HelpConfigKV(ctx, "", "", envOnly)
}
// implements madmin.SetConfigKV()
func (ac AdminClient) setConfigKV(ctx context.Context, kv string) (restart bool, err error) {
return ac.Client.SetConfigKV(ctx, kv)
}
// implements madmin.DelConfigKV()
func (ac AdminClient) delConfigKV(ctx context.Context, kv string) (err error) {
_, err = ac.Client.DelConfigKV(ctx, kv)
return err
}
// implements madmin.ServiceRestart()
func (ac AdminClient) serviceRestart(ctx context.Context) (err error) {
return ac.Client.ServiceRestart(ctx)
}
// implements madmin.ServerInfo()
func (ac AdminClient) serverInfo(ctx context.Context) (madmin.InfoMessage, error) {
return ac.Client.ServerInfo(ctx)
}
// implements madmin.StartProfiling()
func (ac AdminClient) startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
return ac.Client.StartProfiling(ctx, profiler)
}
// implements madmin.DownloadProfilingData()
func (ac AdminClient) stopProfiling(ctx context.Context) (io.ReadCloser, error) {
return ac.Client.DownloadProfilingData(ctx)
}
// implements madmin.ServiceTrace()
func (ac AdminClient) serviceTrace(ctx context.Context, threshold int64, _, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
thresholdT := time.Duration(threshold)
tracingOptions := madmin.ServiceTraceOpts{
S3: true,
OnlyErrors: errTrace,
Internal: internal,
Storage: storage,
OS: os,
Threshold: thresholdT,
}
return ac.Client.ServiceTrace(ctx, tracingOptions)
}
// implements madmin.GetLogs()
func (ac AdminClient) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return ac.Client.GetLogs(ctx, node, lineCnt, logKind)
}
// implements madmin.AddServiceAccount()
func (ac AdminClient) addServiceAccount(ctx context.Context, policy string, user string, accessKey string, secretKey string, name string, description string, expiry *time.Time, comment string) (madmin.Credentials, error) {
return ac.Client.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
Policy: []byte(policy),
TargetUser: user,
AccessKey: accessKey,
SecretKey: secretKey,
Name: name,
Description: description,
Expiration: expiry,
Comment: comment,
})
}
// implements madmin.ListServiceAccounts()
func (ac AdminClient) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
return ac.Client.ListServiceAccounts(ctx, user)
}
// implements madmin.DeleteServiceAccount()
func (ac AdminClient) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
return ac.Client.DeleteServiceAccount(ctx, serviceAccount)
}
// implements madmin.InfoServiceAccount()
func (ac AdminClient) infoServiceAccount(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error) {
return ac.Client.InfoServiceAccount(ctx, serviceAccount)
}
// implements madmin.UpdateServiceAccount()
func (ac AdminClient) updateServiceAccount(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error {
return ac.Client.UpdateServiceAccount(ctx, serviceAccount, opts)
}
// AccountInfo implements madmin.AccountInfo()
func (ac AdminClient) AccountInfo(ctx context.Context) (madmin.AccountInfo, error) {
return ac.Client.AccountInfo(ctx, madmin.AccountOpts{})
}
func (ac AdminClient) heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
forceStart, forceStop bool,
) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return ac.Client.Heal(ctx, bucket, prefix, healOpts, clientToken, forceStart, forceStop)
}
// listRemoteBuckets - return a list of remote buckets
func (ac AdminClient) listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error) {
return ac.Client.ListRemoteTargets(ctx, bucket, arnType)
}
// getRemoteBucket - gets remote bucked based on a given bucket name
func (ac AdminClient) getRemoteBucket(ctx context.Context, bucket, arnType string) (*madmin.BucketTarget, error) {
targets, err := ac.Client.ListRemoteTargets(ctx, bucket, arnType)
if err != nil {
return nil, err
}
if len(targets) > 0 {
return &targets[0], nil
}
return nil, err
}
// removeRemoteBucket removes a remote target associated with particular ARN for this bucket
func (ac AdminClient) removeRemoteBucket(ctx context.Context, bucket, arn string) error {
return ac.Client.RemoveRemoteTarget(ctx, bucket, arn)
}
// addRemoteBucket sets up a remote target for this bucket
func (ac AdminClient) addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) {
return ac.Client.SetRemoteTarget(ctx, bucket, target)
}
func (ac AdminClient) setBucketQuota(ctx context.Context, bucket string, quota *madmin.BucketQuota) error {
return ac.Client.SetBucketQuota(ctx, bucket, quota)
}
func (ac AdminClient) getBucketQuota(ctx context.Context, bucket string) (madmin.BucketQuota, error) {
return ac.Client.GetBucketQuota(ctx, bucket)
}
// serverHealthInfo implements mc.ServerHealthInfo - Connect to a minio server and call Health Info Management API
func (ac AdminClient) serverHealthInfo(ctx context.Context, deadline time.Duration) (interface{}, string, error) {
info := madmin.HealthInfo{}
var healthInfo interface{}
var version string
var tryCount int
for info.Version == "" && tryCount < 10 {
var resp *http.Response
var err error
resp, version, err = ac.Client.ServerHealthInfo(ctx, madmin.HealthDataTypesList, deadline, "")
if err != nil {
return nil, version, err
}
decoder := json.NewDecoder(resp.Body)
for {
if err = decoder.Decode(&info); err != nil {
break
}
}
tryCount++
time.Sleep(2 * time.Second)
}
if info.Version == "" {
return nil, "", ErrHealthReportFail
}
healthInfo = info
return healthInfo, version, nil
}
// implements madmin.listTiers()
func (ac AdminClient) listTiers(ctx context.Context) ([]*madmin.TierConfig, error) {
return ac.Client.ListTiers(ctx)
}
// implements madmin.tierStats()
func (ac AdminClient) tierStats(ctx context.Context) ([]madmin.TierInfo, error) {
return ac.Client.TierStats(ctx)
}
// implements madmin.AddTier()
func (ac AdminClient) addTier(ctx context.Context, cfg *madmin.TierConfig) error {
return ac.Client.AddTier(ctx, cfg)
}
// implements madmin.Inspect()
func (ac AdminClient) inspect(ctx context.Context, insOpts madmin.InspectOptions) ([]byte, io.ReadCloser, error) {
return ac.Client.Inspect(ctx, insOpts)
}
// implements madmin.EditTier()
func (ac AdminClient) editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error {
return ac.Client.EditTier(ctx, tierName, creds)
}
// implements madmin.VerifyTier()
func (ac AdminClient) verifyTierStatus(ctx context.Context, tierName string) error {
return ac.Client.VerifyTier(ctx, tierName)
}
// implements madmin.RemoveTier()
func (ac AdminClient) removeTier(ctx context.Context, tierName string) error {
return ac.Client.RemoveTier(ctx, tierName)
}
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
}
// newAdminFromCreds Creates a minio client using custom credentials for connecting to a remote host
func newAdminFromCreds(accessKey, secretKey, endpoint string, tlsEnabled bool) (*madmin.AdminClient, error) {
minioClient, err := madmin.NewWithOptions(endpoint, &madmin.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: tlsEnabled,
})
if err != nil {
return nil, err
}
minioClient.SetAppInfo(globalAppName, pkg.Version)
return minioClient, 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
}
func (ac AdminClient) speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error) {
return ac.Client.Speedtest(ctx, opts)
}
// Site Replication
func (ac AdminClient) getSiteReplicationInfo(ctx context.Context) (*madmin.SiteReplicationInfo, error) {
res, err := ac.Client.SiteReplicationInfo(ctx)
if err != nil {
return nil, err
}
return &madmin.SiteReplicationInfo{
Enabled: res.Enabled,
Name: res.Name,
Sites: res.Sites,
ServiceAccountAccessKey: res.ServiceAccountAccessKey,
}, nil
}
func (ac AdminClient) addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite, opts madmin.SRAddOptions) (*madmin.ReplicateAddStatus, error) {
res, err := ac.Client.SiteReplicationAdd(ctx, sites, opts)
if err != nil {
return nil, err
}
return &madmin.ReplicateAddStatus{
Success: res.Success,
Status: res.Status,
ErrDetail: res.ErrDetail,
InitialSyncErrorMessage: res.InitialSyncErrorMessage,
}, nil
}
func (ac AdminClient) editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo, opts madmin.SREditOptions) (*madmin.ReplicateEditStatus, error) {
res, err := ac.Client.SiteReplicationEdit(ctx, site, opts)
if err != nil {
return nil, err
}
return &madmin.ReplicateEditStatus{
Success: res.Success,
Status: res.Status,
ErrDetail: res.ErrDetail,
}, nil
}
func (ac AdminClient) deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error) {
res, err := ac.Client.SiteReplicationRemove(ctx, removeReq)
if err != nil {
return nil, err
}
return &madmin.ReplicateRemoveStatus{
Status: res.Status,
ErrDetail: res.ErrDetail,
}, nil
}
func (ac AdminClient) getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error) {
res, err := ac.Client.SRStatusInfo(ctx, params)
if err != nil {
return nil, err
}
return &res, nil
}
func (ac AdminClient) kmsStatus(ctx context.Context) (madmin.KMSStatus, error) {
return ac.Client.KMSStatus(ctx)
}
func (ac AdminClient) kmsMetrics(ctx context.Context) (*madmin.KMSMetrics, error) {
return ac.Client.KMSMetrics(ctx)
}
func (ac AdminClient) kmsAPIs(ctx context.Context) ([]madmin.KMSAPI, error) {
return ac.Client.KMSAPIs(ctx)
}
func (ac AdminClient) kmsVersion(ctx context.Context) (*madmin.KMSVersion, error) {
return ac.Client.KMSVersion(ctx)
}
func (ac AdminClient) createKey(ctx context.Context, key string) error {
return ac.Client.CreateKey(ctx, key)
}
func (ac AdminClient) listKeys(ctx context.Context, pattern string) ([]madmin.KMSKeyInfo, error) {
return ac.Client.ListKeys(ctx, pattern)
}
func (ac AdminClient) keyStatus(ctx context.Context, key string) (*madmin.KMSKeyStatus, error) {
return ac.Client.GetKeyStatus(ctx, key)
}
func (ac AdminClient) addOrUpdateIDPConfig(ctx context.Context, idpType, cfgName, cfgData string, update bool) (restart bool, err error) {
return ac.Client.AddOrUpdateIDPConfig(ctx, idpType, cfgName, cfgData, update)
}
func (ac AdminClient) listIDPConfig(ctx context.Context, idpType string) ([]madmin.IDPListItem, error) {
return ac.Client.ListIDPConfig(ctx, idpType)
}
func (ac AdminClient) deleteIDPConfig(ctx context.Context, idpType, cfgName string) (restart bool, err error) {
return ac.Client.DeleteIDPConfig(ctx, idpType, cfgName)
}
func (ac AdminClient) getIDPConfig(ctx context.Context, idpType, cfgName string) (c madmin.IDPConfig, err error) {
return ac.Client.GetIDPConfig(ctx, idpType, cfgName)
}
func (ac AdminClient) getLDAPPolicyEntities(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
return ac.Client.GetLDAPPolicyEntities(ctx, query)
}

View File

@@ -1,495 +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"
"io"
"path"
"strings"
"time"
"github.com/minio/minio-go/v7/pkg/replication"
"github.com/minio/minio-go/v7/pkg/sse"
xnet "github.com/minio/pkg/v3/net"
"github.com/minio/console/models"
"github.com/minio/console/pkg"
"github.com/minio/console/pkg/auth"
"github.com/minio/console/pkg/auth/ldap"
xjwt "github.com/minio/console/pkg/auth/token"
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/lifecycle"
"github.com/minio/minio-go/v7/pkg/notification"
"github.com/minio/minio-go/v7/pkg/tags"
)
func init() {
// All minio-go API operations shall be performed only once,
// another way to look at this is we are turning off retries.
minio.MaxRetry = 1
}
// MinioClient interface with all functions to be implemented
// by mock when testing, it should include all MinioClient respective api calls
// that are used within this project.
type MinioClient interface {
listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error)
makeBucketWithContext(ctx context.Context, bucketName, location string, objectLocking bool) error
setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error
removeBucket(ctx context.Context, bucketName string) error
getBucketNotification(ctx context.Context, bucketName string) (config notification.Configuration, err error)
getBucketPolicy(ctx context.Context, bucketName string) (string, error)
listObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
putObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error)
putObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.PutObjectLegalHoldOptions) error
putObjectRetention(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error
statObject(ctx context.Context, bucketName, prefix string, opts minio.GetObjectOptions) (objectInfo minio.ObjectInfo, err error)
setBucketEncryption(ctx context.Context, bucketName string, config *sse.Configuration) error
removeBucketEncryption(ctx context.Context, bucketName string) error
getBucketEncryption(ctx context.Context, bucketName string) (*sse.Configuration, error)
putObjectTagging(ctx context.Context, bucketName, objectName string, otags *tags.Tags, opts minio.PutObjectTaggingOptions) error
getObjectTagging(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error)
setObjectLockConfig(ctx context.Context, bucketName string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit) error
getBucketObjectLockConfig(ctx context.Context, bucketName string) (mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error)
getObjectLockConfig(ctx context.Context, bucketName string) (lock string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error)
getLifecycleRules(ctx context.Context, bucketName string) (lifecycle *lifecycle.Configuration, err error)
setBucketLifecycle(ctx context.Context, bucketName string, config *lifecycle.Configuration) error
copyObject(ctx context.Context, dst minio.CopyDestOptions, src minio.CopySrcOptions) (minio.UploadInfo, error)
GetBucketTagging(ctx context.Context, bucketName string) (*tags.Tags, error)
SetBucketTagging(ctx context.Context, bucketName string, tags *tags.Tags) error
RemoveBucketTagging(ctx context.Context, bucketName string) error
}
// Interface implementation
//
// Define the structure of a minIO Client and define the functions that are actually used
// from minIO api.
type minioClient struct {
client *minio.Client
}
func (c minioClient) GetBucketTagging(ctx context.Context, bucketName string) (*tags.Tags, error) {
return c.client.GetBucketTagging(ctx, bucketName)
}
func (c minioClient) SetBucketTagging(ctx context.Context, bucketName string, tags *tags.Tags) error {
return c.client.SetBucketTagging(ctx, bucketName, tags)
}
func (c minioClient) RemoveBucketTagging(ctx context.Context, bucketName string) error {
return c.client.RemoveBucketTagging(ctx, bucketName)
}
// implements minio.ListBuckets(ctx)
func (c minioClient) listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error) {
return c.client.ListBuckets(ctx)
}
// implements minio.MakeBucketWithContext(ctx, bucketName, location, objectLocking)
func (c minioClient) makeBucketWithContext(ctx context.Context, bucketName, location string, objectLocking bool) error {
return c.client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{
Region: location,
ObjectLocking: objectLocking,
})
}
// implements minio.SetBucketPolicyWithContext(ctx, bucketName, policy)
func (c minioClient) setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error {
return c.client.SetBucketPolicy(ctx, bucketName, policy)
}
// implements minio.RemoveBucket(bucketName)
func (c minioClient) removeBucket(ctx context.Context, bucketName string) error {
return c.client.RemoveBucket(ctx, bucketName)
}
// implements minio.GetBucketNotification(bucketName)
func (c minioClient) getBucketNotification(ctx context.Context, bucketName string) (config notification.Configuration, err error) {
return c.client.GetBucketNotification(ctx, bucketName)
}
// implements minio.GetBucketPolicy(bucketName)
func (c minioClient) getBucketPolicy(ctx context.Context, bucketName string) (string, error) {
return c.client.GetBucketPolicy(ctx, bucketName)
}
// implements minio.getBucketVersioning(ctx, bucketName)
func (c minioClient) getBucketVersioning(ctx context.Context, bucketName string) (minio.BucketVersioningConfiguration, error) {
return c.client.GetBucketVersioning(ctx, bucketName)
}
// implements minio.getBucketVersioning(ctx, bucketName)
func (c minioClient) getBucketReplication(ctx context.Context, bucketName string) (replication.Config, error) {
return c.client.GetBucketReplication(ctx, bucketName)
}
// implements minio.listObjects(ctx)
func (c minioClient) listObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
return c.client.ListObjects(ctx, bucket, opts)
}
func (c minioClient) getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
return c.client.GetObjectRetention(ctx, bucketName, objectName, versionID)
}
func (c minioClient) getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
return c.client.GetObjectLegalHold(ctx, bucketName, objectName, opts)
}
func (c minioClient) putObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) {
return c.client.PutObject(ctx, bucketName, objectName, reader, objectSize, opts)
}
func (c minioClient) putObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.PutObjectLegalHoldOptions) error {
return c.client.PutObjectLegalHold(ctx, bucketName, objectName, opts)
}
func (c minioClient) putObjectRetention(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error {
return c.client.PutObjectRetention(ctx, bucketName, objectName, opts)
}
func (c minioClient) statObject(ctx context.Context, bucketName, prefix string, opts minio.GetObjectOptions) (objectInfo minio.ObjectInfo, err error) {
return c.client.StatObject(ctx, bucketName, prefix, opts)
}
// implements minio.SetBucketEncryption(ctx, bucketName, config)
func (c minioClient) setBucketEncryption(ctx context.Context, bucketName string, config *sse.Configuration) error {
return c.client.SetBucketEncryption(ctx, bucketName, config)
}
// implements minio.RemoveBucketEncryption(ctx, bucketName)
func (c minioClient) removeBucketEncryption(ctx context.Context, bucketName string) error {
return c.client.RemoveBucketEncryption(ctx, bucketName)
}
// implements minio.GetBucketEncryption(ctx, bucketName, config)
func (c minioClient) getBucketEncryption(ctx context.Context, bucketName string) (*sse.Configuration, error) {
return c.client.GetBucketEncryption(ctx, bucketName)
}
func (c minioClient) putObjectTagging(ctx context.Context, bucketName, objectName string, otags *tags.Tags, opts minio.PutObjectTaggingOptions) error {
return c.client.PutObjectTagging(ctx, bucketName, objectName, otags, opts)
}
func (c minioClient) getObjectTagging(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) {
return c.client.GetObjectTagging(ctx, bucketName, objectName, opts)
}
func (c minioClient) setObjectLockConfig(ctx context.Context, bucketName string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit) error {
return c.client.SetObjectLockConfig(ctx, bucketName, mode, validity, unit)
}
func (c minioClient) getBucketObjectLockConfig(ctx context.Context, bucketName string) (mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error) {
return c.client.GetBucketObjectLockConfig(ctx, bucketName)
}
func (c minioClient) getObjectLockConfig(ctx context.Context, bucketName string) (lock string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error) {
return c.client.GetObjectLockConfig(ctx, bucketName)
}
func (c minioClient) getLifecycleRules(ctx context.Context, bucketName string) (lifecycle *lifecycle.Configuration, err error) {
return c.client.GetBucketLifecycle(ctx, bucketName)
}
func (c minioClient) setBucketLifecycle(ctx context.Context, bucketName string, config *lifecycle.Configuration) error {
return c.client.SetBucketLifecycle(ctx, bucketName, config)
}
func (c minioClient) copyObject(ctx context.Context, dst minio.CopyDestOptions, src minio.CopySrcOptions) (minio.UploadInfo, error) {
return c.client.CopyObject(ctx, dst, src)
}
// MCClient interface with all functions to be implemented
// by mock when testing, it should include all mc/S3Client respective api calls
// that are used within this project.
type MCClient interface {
addNotificationConfig(ctx context.Context, arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
removeNotificationConfig(ctx context.Context, arn string, event string, prefix string, suffix string) *probe.Error
watch(ctx context.Context, options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult
list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent
get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error)
shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error)
setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error
}
// Interface implementation
//
// Define the structure of a mc S3Client and define the functions that are actually used
// from mcS3client api.
type mcClient struct {
client *mc.S3Client
}
// implements S3Client.AddNotificationConfig()
func (c mcClient) addNotificationConfig(ctx context.Context, arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error {
return c.client.AddNotificationConfig(ctx, arn, events, prefix, suffix, ignoreExisting)
}
// implements S3Client.RemoveNotificationConfig()
func (c mcClient) removeNotificationConfig(ctx context.Context, arn string, event string, prefix string, suffix string) *probe.Error {
return c.client.RemoveNotificationConfig(ctx, arn, event, prefix, suffix)
}
func (c mcClient) watch(ctx context.Context, options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return c.client.Watch(ctx, options)
}
func (c mcClient) setReplication(ctx context.Context, cfg *replication.Config, opts replication.Options) *probe.Error {
return c.client.SetReplication(ctx, cfg, opts)
}
func (c mcClient) deleteAllReplicationRules(ctx context.Context) *probe.Error {
return c.client.RemoveReplication(ctx)
}
func (c mcClient) setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error {
return c.client.SetVersion(ctx, status, excludePrefix, excludeFolders)
}
func (c mcClient) remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
return c.client.Remove(ctx, isIncomplete, isRemoveBucket, isBypass, forceDelete, contentCh)
}
func (c mcClient) list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent {
return c.client.List(ctx, opts)
}
func (c mcClient) get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error) {
rd, _, err := c.client.Get(ctx, opts)
return rd, err
}
func (c mcClient) shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) {
return c.client.ShareDownload(ctx, versionID, expires)
}
// ConsoleCredentialsI interface with all functions to be implemented
// by mock when testing, it should include all needed consoleCredentials.Login api calls
// that are used within this project.
type ConsoleCredentialsI interface {
Get() (credentials.Value, error)
Expire()
GetAccountAccessKey() string
}
// Interface implementation
type ConsoleCredentials struct {
ConsoleCredentials *credentials.Credentials
AccountAccessKey string
}
func (c ConsoleCredentials) GetAccountAccessKey() string {
return c.AccountAccessKey
}
// Get implements *Login.Get()
func (c ConsoleCredentials) Get() (credentials.Value, error) {
return c.ConsoleCredentials.Get()
}
// Expire implements *Login.Expire()
func (c ConsoleCredentials) Expire() {
c.ConsoleCredentials.Expire()
}
// consoleSTSAssumeRole it's a STSAssumeRole wrapper, in general
// there's no need to use this struct anywhere else in the project, it's only required
// for passing a custom *http.Client to *credentials.STSAssumeRole
type consoleSTSAssumeRole struct {
stsAssumeRole *credentials.STSAssumeRole
}
func (s consoleSTSAssumeRole) Retrieve() (credentials.Value, error) {
return s.stsAssumeRole.Retrieve()
}
func (s consoleSTSAssumeRole) IsExpired() bool {
return s.stsAssumeRole.IsExpired()
}
func stsCredentials(minioURL, accessKey, secretKey, location, clientIP string) (*credentials.Credentials, error) {
if accessKey == "" || secretKey == "" {
return nil, errors.New("credentials endpoint, access and secret key are mandatory for AssumeRoleSTS")
}
opts := credentials.STSAssumeRoleOptions{
AccessKey: accessKey,
SecretKey: secretKey,
Location: location,
DurationSeconds: int(xjwt.GetConsoleSTSDuration().Seconds()),
}
stsAssumeRole := &credentials.STSAssumeRole{
Client: GetConsoleHTTPClient(clientIP),
STSEndpoint: minioURL,
Options: opts,
}
consoleSTSWrapper := consoleSTSAssumeRole{stsAssumeRole: stsAssumeRole}
return credentials.New(consoleSTSWrapper), nil
}
func NewConsoleCredentials(accessKey, secretKey, location, clientIP string) (*credentials.Credentials, error) {
minioURL := getMinIOServer()
// Future authentication methods can be added under this switch statement
switch {
// LDAP authentication for Console
case ldap.GetLDAPEnabled():
{
creds, err := auth.GetCredentialsFromLDAP(GetConsoleHTTPClient(clientIP), minioURL, accessKey, secretKey)
if err != nil {
return nil, err
}
// We verify if LDAP credentials are correct and no error is returned
_, err = creds.Get()
if err != nil && strings.Contains(strings.ToLower(err.Error()), "not found") {
// We try to use STS Credentials in case LDAP credentials are incorrect.
stsCreds, errSTS := stsCredentials(minioURL, accessKey, secretKey, location, clientIP)
// If there is an error with STS too, then we return the original LDAP error
if errSTS != nil {
LogError("error in STS credentials for LDAP case: %v ", errSTS)
// We return LDAP result
return creds, nil
}
_, err := stsCreds.Get()
// There is an error with STS credentials, We return the result of LDAP as STS is not a priority in this case.
if err != nil {
return creds, nil
}
return stsCreds, nil
}
return creds, nil
}
// default authentication for Console is via STS (Security Token Service) against MinIO
default:
{
return stsCredentials(minioURL, accessKey, secretKey, location, clientIP)
}
}
}
// getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the
// provided session token, this is useful for running the Expire() or IsExpired() operations
func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials {
if claims == nil {
return credentials.NewStaticV4("", "", "")
}
return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken)
}
// newMinioClient creates a new MinIO client based on the ConsoleCredentials extracted
// from the provided session token
func newMinioClient(claims *models.Principal, clientIP string) (*minio.Client, error) {
creds := getConsoleCredentialsFromSession(claims)
endpoint := getMinIOEndpoint()
secure := getMinIOEndpointIsSecure()
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: creds,
Secure: secure,
Transport: GetConsoleHTTPClient(clientIP).Transport,
})
if err != nil {
return nil, err
}
// set user-agent to differentiate Console UI requests for auditing.
minioClient.SetAppInfo("MinIO Console", pkg.Version)
return minioClient, nil
}
// computeObjectURLWithoutEncode returns a MinIO url containing the object filename without encoding
func computeObjectURLWithoutEncode(bucketName, prefix string) (string, error) {
u, err := xnet.ParseHTTPURL(getMinIOServer())
if err != nil {
return "", fmt.Errorf("the provided endpoint: '%s' is invalid", getMinIOServer())
}
var p string
if strings.TrimSpace(bucketName) != "" {
p = path.Join(p, bucketName)
}
if strings.TrimSpace(prefix) != "" {
p = pathJoinFinalSlash(p, prefix)
}
return u.String() + "/" + p, nil
}
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
func newS3BucketClient(claims *models.Principal, bucketName string, prefix string, clientIP string) (*mc.S3Client, error) {
if claims == nil {
return nil, fmt.Errorf("the provided credentials are invalid")
}
// It's very important to avoid encoding the prefix since the minio client will encode the path itself
objectURL, err := computeObjectURLWithoutEncode(bucketName, prefix)
if err != nil {
return nil, fmt.Errorf("the provided endpoint is invalid")
}
s3Config := newS3Config(objectURL, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, clientIP)
client, pErr := mc.S3New(s3Config)
if pErr != nil {
return nil, pErr.Cause
}
s3Client, ok := client.(*mc.S3Client)
if !ok {
return nil, fmt.Errorf("the provided url doesn't point to a S3 server")
}
return s3Client, nil
}
// pathJoinFinalSlash - like path.Join() but retains trailing slashSeparator of the last element
func pathJoinFinalSlash(elem ...string) string {
if len(elem) > 0 {
if strings.HasSuffix(elem[len(elem)-1], SlashSeparator) {
return path.Join(elem...) + SlashSeparator
}
}
return path.Join(elem...)
}
// Deprecated
// newS3Config simply creates a new Config struct using the passed
// parameters.
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, clientIP string) *mc.Config {
// We have a valid alias and hostConfig. We populate the/
// consoleCredentials from the match found in the config file.
return &mc.Config{
HostURL: endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
SessionToken: sessionToken,
Signature: "S3v4",
AppName: globalAppName,
AppVersion: pkg.Version,
Insecure: isLocalIPEndpoint(endpoint),
Transport: &ConsoleTransport{
ClientIP: clientIP,
Transport: GlobalTransport,
},
}
}

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,309 +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/tls"
"crypto/x509"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/minio/console/pkg/auth/idp/oauth2"
xcerts "github.com/minio/pkg/v3/certs"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
)
var (
// Port console default port
Port = "9090"
// Hostname console hostname
// avoid listening on 0.0.0.0 by default
// instead listen on all IPv4 and IPv6
// - Hostname should be empty.
Hostname = ""
// TLSPort console tls port
TLSPort = "9443"
// TLSRedirect console tls redirect rule
TLSRedirect = "on"
ConsoleResourceName = "console-ui"
)
var (
// GlobalRootCAs is CA root certificates, a nil value means system certs pool will be used
GlobalRootCAs *x509.CertPool
// GlobalPublicCerts has certificates Console will use to serve clients
GlobalPublicCerts []*x509.Certificate
// GlobalTLSCertsManager custom TLS Manager for SNI support
GlobalTLSCertsManager *xcerts.Manager
// GlobalTransport is common transport used for all HTTP calls, this is set via
// MinIO server to be the correct transport, however we still define some defaults
// here just in case.
GlobalTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 15 * time.Second,
}).DialContext,
MaxIdleConns: 1024,
MaxIdleConnsPerHost: 1024,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
DisableCompression: true, // Set to avoid auto-decompression
TLSClientConfig: &tls.Config{
// Can't use SSLv3 because of POODLE and BEAST
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
// Console runs in the same pod/node as MinIO this is acceptable.
InsecureSkipVerify: true,
RootCAs: GlobalRootCAs,
},
}
)
// MinIOConfig represents application configuration passed in from the MinIO
// server to the console.
type MinIOConfig struct {
OpenIDProviders oauth2.OpenIDPCfg
}
// GlobalMinIOConfig is the global application configuration passed in from the
// MinIO server.
var GlobalMinIOConfig MinIOConfig
func getMinIOServer() string {
return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000"))
}
func getSubnetProxy() string {
return strings.TrimSpace(env.Get(ConsoleSubnetProxy, ""))
}
func GetMinIORegion() string {
return strings.TrimSpace(env.Get(ConsoleMinIORegion, ""))
}
func getMinIOEndpoint() string {
u, err := xnet.ParseHTTPURL(getMinIOServer())
if err != nil {
panic(err)
}
return u.Host
}
func getMinIOEndpointIsSecure() bool {
u, err := xnet.ParseHTTPURL(getMinIOServer())
if err != nil {
panic(err)
}
return u.Scheme == "https"
}
// GetHostname gets console hostname set on env variable,
// default one or defined on run command
func GetHostname() string {
return strings.ToLower(env.Get(ConsoleHostname, Hostname))
}
// GetPort gets console por set on env variable
// or default one
func GetPort() int {
port, err := strconv.Atoi(env.Get(ConsolePort, Port))
if err != nil {
port = 9090
}
return port
}
// GetTLSPort gets console tls port set on env variable
// or default one
func GetTLSPort() int {
port, err := strconv.Atoi(env.Get(ConsoleTLSPort, TLSPort))
if err != nil {
port = 9443
}
return port
}
// If GetTLSRedirect is set to true, then only allow HTTPS requests. Default is true.
func GetTLSRedirect() string {
return strings.ToLower(env.Get(ConsoleSecureTLSRedirect, TLSRedirect))
}
// Get secure middleware env variable configurations
func GetSecureAllowedHosts() []string {
allowedHosts := env.Get(ConsoleSecureAllowedHosts, "")
if allowedHosts != "" {
return strings.Split(allowedHosts, ",")
}
return []string{}
}
// AllowedHostsAreRegex determines, if the provided AllowedHosts slice contains valid regular expressions. Default is false.
func GetSecureAllowedHostsAreRegex() bool {
return strings.ToLower(env.Get(ConsoleSecureAllowedHostsAreRegex, "off")) == "on"
}
// If FrameDeny is set to true, adds the X-Frame-Options header with the value of `DENY`. Default is true.
func GetSecureFrameDeny() bool {
return strings.ToLower(env.Get(ConsoleSecureFrameDeny, "on")) == "on"
}
// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value `nosniff`. Default is true.
func GetSecureContentTypeNonSniff() bool {
return strings.ToLower(env.Get(ConsoleSecureContentTypeNoSniff, "on")) == "on"
}
// If BrowserXssFilter is true, adds the X-XSS-Protection header with the value `1; mode=block`. Default is true.
func GetSecureBrowserXSSFilter() bool {
return strings.ToLower(env.Get(ConsoleSecureBrowserXSSFilter, "on")) == "on"
}
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
// Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request which can be
// later retrieved using the Nonce function.
func GetSecureContentSecurityPolicy() string {
return env.Get(ConsoleSecureContentSecurityPolicy, "")
}
// ContentSecurityPolicyReportOnly allows the Content-Security-Policy-Report-Only header value to be set with a custom value. Default is "".
func GetSecureContentSecurityPolicyReportOnly() string {
return env.Get(ConsoleSecureContentSecurityPolicyReportOnly, "")
}
// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request.
func GetSecureHostsProxyHeaders() []string {
allowedHosts := env.Get(ConsoleSecureHostsProxyHeaders, "")
if allowedHosts != "" {
return strings.Split(allowedHosts, ",")
}
return []string{}
}
// TLSHost is the host name that is used to redirect HTTP requests to HTTPS. Default is "", which indicates to use the same host.
func GetSecureTLSHost() string {
tlsHost := env.Get(ConsoleSecureTLSHost, "")
if tlsHost == "" && Hostname != "" {
return net.JoinHostPort(Hostname, TLSPort)
}
return ""
}
// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.
func GetSecureSTSSeconds() int64 {
seconds, err := strconv.Atoi(env.Get(ConsoleSecureSTSSeconds, "0"))
if err != nil {
seconds = 0
}
return int64(seconds)
}
// If STSIncludeSubdomains is set to true, the `includeSubdomains` will be appended to the Strict-Transport-Security header. Default is false.
func GetSecureSTSIncludeSubdomains() bool {
return strings.ToLower(env.Get(ConsoleSecureSTSIncludeSubdomains, "off")) == "on"
}
// If STSPreload is set to true, the `preload` flag will be appended to the Strict-Transport-Security header. Default is false.
func GetSecureSTSPreload() bool {
return strings.ToLower(env.Get(ConsoleSecureSTSPreload, "off")) == "on"
}
// If TLSTemporaryRedirect is true, the a 302 will be used while redirecting. Default is false (301).
func GetSecureTLSTemporaryRedirect() bool {
return strings.ToLower(env.Get(ConsoleSecureTLSTemporaryRedirect, "off")) == "on"
}
// STS header is only included when the connection is HTTPS.
func GetSecureForceSTSHeader() bool {
return strings.ToLower(env.Get(ConsoleSecureForceSTSHeader, "off")) == "on"
}
// ReferrerPolicy allows the Referrer-Policy header with the value to be set with a custom value. Default is "".
func GetSecureReferrerPolicy() string {
return env.Get(ConsoleSecureReferrerPolicy, "")
}
// FeaturePolicy allows the Feature-Policy header with the value to be set with a custom value. Default is "".
func GetSecureFeaturePolicy() string {
return env.Get(ConsoleSecureFeaturePolicy, "")
}
func getLogSearchAPIToken() string {
if v := env.Get(ConsoleLogQueryAuthToken, ""); v != "" {
return v
}
return env.Get(LogSearchQueryAuthToken, "")
}
func getLogSearchURL() string {
return env.Get(ConsoleLogQueryURL, "")
}
func getPrometheusURL() string {
return env.Get(PrometheusURL, "")
}
func getPrometheusAuthToken() string {
return env.Get(PrometheusAuthToken, "")
}
func getPrometheusJobID() string {
return env.Get(PrometheusJobID, "minio-job")
}
func getPrometheusExtraLabels() string {
return env.Get(PrometheusExtraLabels, "")
}
func getMaxConcurrentUploadsLimit() int64 {
cu, err := strconv.ParseInt(env.Get(ConsoleMaxConcurrentUploads, "10"), 10, 64)
if err != nil {
return 10
}
return cu
}
func getMaxConcurrentDownloadsLimit() int64 {
cu, err := strconv.ParseInt(env.Get(ConsoleMaxConcurrentDownloads, "20"), 10, 64)
if err != nil {
return 20
}
return cu
}
func getConsoleDevMode() bool {
return strings.ToLower(env.Get(ConsoleDevMode, "off")) == "on"
}
func getConsoleAnimatedLogin() bool {
return strings.ToLower(env.Get(ConsoleAnimatedLogin, "on")) == "on"
}
func getConsoleBrowserRedirectURL() string {
return env.Get(ConsoleBrowserRedirectURL, "")
}

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,619 +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 all users handlers
registerUsersHandlers(api)
// Register groups handlers
registerGroupsHandlers(api)
// Register policies handlers
registersPoliciesHandler(api)
// Register configurations handlers
registerConfigHandlers(api)
// Register bucket events handlers
registerBucketEventsHandlers(api)
// Register bucket lifecycle handlers
registerBucketsLifecycleHandlers(api)
// Register service handlers
registerServiceHandlers(api)
// Register session handlers
registerSessionHandlers(api)
// Register admin info handlers
registerAdminInfoHandlers(api)
// Register admin arns handlers
registerAdminArnsHandlers(api)
// Register admin notification endpoints handlers
registerAdminNotificationEndpointsHandlers(api)
// Register admin Service Account Handlers
registerServiceAccountsHandlers(api)
// Register admin remote buckets
registerAdminBucketRemoteHandlers(api)
// Register admin log search
registerLogSearchHandlers(api)
// Register admin subnet handlers
registerSubnetHandlers(api)
// Register admin KMS handlers
registerKMSHandlers(api)
// Register admin IDP handlers
registerIDPHandlers(api)
// Register Account handlers
registerAdminTiersHandlers(api)
// Register Inspect Handler
registerInspectHandler(api)
// Register nodes handlers
registerNodesHandler(api)
registerSiteReplicationHandler(api)
registerSiteReplicationStatusHandler(api)
// Register Support Handler
registerSupportHandlers(api)
// Operator Console
// Register Object's Handlers
registerObjectsHandlers(api)
// Register Bucket Quota's Handlers
registerBucketQuotaHandlers(api)
// Register Account handlers
registerAccountHandlers(api)
registerReleasesHandlers(api)
registerPublicObjectsHandlers(api)
api.PreServerShutdown = func() {}
api.ServerShutdown = func() {}
// do an initial subnet plan caching
fetchLicensePlan()
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)
newPlan := fmt.Sprintf("<meta name=\"minio-license\" content=\"%s\" />", InstanceLicensePlan.String())
indexPageStr = strings.Replace(indexPageStr, "<meta name=\"minio-license\" content=\"agpl\"/>", newPlan, 1)
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,63 +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
// list of all console environment constants
const (
// Constants for common configuration
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
ConsoleSubnetProxy = "CONSOLE_SUBNET_PROXY"
ConsoleMinIORegion = "CONSOLE_MINIO_REGION"
ConsoleHostname = "CONSOLE_HOSTNAME"
ConsolePort = "CONSOLE_PORT"
ConsoleTLSPort = "CONSOLE_TLS_PORT"
// Constants for Secure middleware
ConsoleSecureAllowedHosts = "CONSOLE_SECURE_ALLOWED_HOSTS"
ConsoleSecureAllowedHostsAreRegex = "CONSOLE_SECURE_ALLOWED_HOSTS_ARE_REGEX"
ConsoleSecureFrameDeny = "CONSOLE_SECURE_FRAME_DENY"
ConsoleSecureContentTypeNoSniff = "CONSOLE_SECURE_CONTENT_TYPE_NO_SNIFF"
ConsoleSecureBrowserXSSFilter = "CONSOLE_SECURE_BROWSER_XSS_FILTER"
ConsoleSecureContentSecurityPolicy = "CONSOLE_SECURE_CONTENT_SECURITY_POLICY"
ConsoleSecureContentSecurityPolicyReportOnly = "CONSOLE_SECURE_CONTENT_SECURITY_POLICY_REPORT_ONLY"
ConsoleSecureHostsProxyHeaders = "CONSOLE_SECURE_HOSTS_PROXY_HEADERS"
ConsoleSecureSTSSeconds = "CONSOLE_SECURE_STS_SECONDS"
ConsoleSecureSTSIncludeSubdomains = "CONSOLE_SECURE_STS_INCLUDE_SUB_DOMAINS"
ConsoleSecureSTSPreload = "CONSOLE_SECURE_STS_PRELOAD"
ConsoleSecureTLSRedirect = "CONSOLE_SECURE_TLS_REDIRECT"
ConsoleSecureTLSHost = "CONSOLE_SECURE_TLS_HOST"
ConsoleSecureTLSTemporaryRedirect = "CONSOLE_SECURE_TLS_TEMPORARY_REDIRECT"
ConsoleSecureForceSTSHeader = "CONSOLE_SECURE_FORCE_STS_HEADER"
ConsoleSecurePublicKey = "CONSOLE_SECURE_PUBLIC_KEY"
ConsoleSecureReferrerPolicy = "CONSOLE_SECURE_REFERRER_POLICY"
ConsoleSecureFeaturePolicy = "CONSOLE_SECURE_FEATURE_POLICY"
ConsoleSecureExpectCTHeader = "CONSOLE_SECURE_EXPECT_CT_HEADER"
PrometheusURL = "CONSOLE_PROMETHEUS_URL"
PrometheusAuthToken = "CONSOLE_PROMETHEUS_AUTH_TOKEN"
PrometheusJobID = "CONSOLE_PROMETHEUS_JOB_ID"
PrometheusExtraLabels = "CONSOLE_PROMETHEUS_EXTRA_LABELS"
ConsoleLogQueryURL = "CONSOLE_LOG_QUERY_URL"
ConsoleLogQueryAuthToken = "CONSOLE_LOG_QUERY_AUTH_TOKEN"
ConsoleMaxConcurrentUploads = "CONSOLE_MAX_CONCURRENT_UPLOADS"
ConsoleMaxConcurrentDownloads = "CONSOLE_MAX_CONCURRENT_DOWNLOADS"
ConsoleDevMode = "CONSOLE_DEV_MODE"
ConsoleAnimatedLogin = "CONSOLE_ANIMATED_LOGIN"
ConsoleBrowserRedirectURL = "CONSOLE_BROWSER_REDIRECT_URL"
LogSearchQueryAuthToken = "LOGSEARCH_QUERY_AUTH_TOKEN"
SlashSeparator = "/"
LocalAddress = "127.0.0.1"
)

View File

@@ -1,556 +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"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/go-openapi/runtime/flagext"
"github.com/go-openapi/swag"
flags "github.com/jessevdk/go-flags"
"golang.org/x/net/netutil"
"github.com/minio/console/api/operations"
)
const (
schemeHTTP = "http"
schemeHTTPS = "https"
schemeUnix = "unix"
)
var defaultSchemes []string
func init() {
defaultSchemes = []string{
schemeHTTP,
}
}
// NewServer creates a new api console server but does not configure it
func NewServer(api *operations.ConsoleAPI) *Server {
s := new(Server)
s.shutdown = make(chan struct{})
s.api = api
s.interrupt = make(chan os.Signal, 1)
return s
}
// ConfigureAPI configures the API and handlers.
func (s *Server) ConfigureAPI() {
if s.api != nil {
s.handler = configureAPI(s.api)
}
}
// ConfigureFlags configures the additional flags defined by the handlers. Needs to be called before the parser.Parse
func (s *Server) ConfigureFlags() {
if s.api != nil {
configureFlags(s.api)
}
}
// Server for the console API
type Server struct {
EnabledListeners []string `long:"scheme" description:"the listeners to enable, this can be repeated and defaults to the schemes in the swagger spec"`
CleanupTimeout time.Duration `long:"cleanup-timeout" description:"grace period for which to wait before killing idle connections" default:"10s"`
GracefulTimeout time.Duration `long:"graceful-timeout" description:"grace period for which to wait before shutting down the server" default:"15s"`
MaxHeaderSize flagext.ByteSize `long:"max-header-size" description:"controls the maximum number of bytes the server will read parsing the request header's keys and values, including the request line. It does not limit the size of the request body." default:"1MiB"`
SocketPath flags.Filename `long:"socket-path" description:"the unix socket to listen on" default:"/var/run/console.sock"`
domainSocketL net.Listener
Host string `long:"host" description:"the IP to listen on" default:"localhost" env:"HOST"`
Port int `long:"port" description:"the port to listen on for insecure connections, defaults to a random value" env:"PORT"`
ListenLimit int `long:"listen-limit" description:"limit the number of outstanding requests"`
KeepAlive time.Duration `long:"keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)" default:"3m"`
ReadTimeout time.Duration `long:"read-timeout" description:"maximum duration before timing out read of the request" default:"30s"`
WriteTimeout time.Duration `long:"write-timeout" description:"maximum duration before timing out write of the response" default:"60s"`
httpServerL []net.Listener
TLSHost string `long:"tls-host" description:"the IP to listen on for tls, when not specified it's the same as --host" env:"TLS_HOST"`
TLSPort int `long:"tls-port" description:"the port to listen on for secure connections, defaults to a random value" env:"TLS_PORT"`
TLSCertificate flags.Filename `long:"tls-certificate" description:"the certificate to use for secure connections" env:"TLS_CERTIFICATE"`
TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure connections" env:"TLS_PRIVATE_KEY"`
TLSCACertificate flags.Filename `long:"tls-ca" description:"the certificate authority file to be used with mutual tls auth" env:"TLS_CA_CERTIFICATE"`
TLSListenLimit int `long:"tls-listen-limit" description:"limit the number of outstanding requests"`
TLSKeepAlive time.Duration `long:"tls-keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)"`
TLSReadTimeout time.Duration `long:"tls-read-timeout" description:"maximum duration before timing out read of the request"`
TLSWriteTimeout time.Duration `long:"tls-write-timeout" description:"maximum duration before timing out write of the response"`
httpsServerL []net.Listener
api *operations.ConsoleAPI
handler http.Handler
hasListeners bool
shutdown chan struct{}
shuttingDown int32
interrupted bool
interrupt chan os.Signal
}
// Logf logs message either via defined user logger or via system one if no user logger is defined.
func (s *Server) Logf(f string, args ...interface{}) {
if s.api != nil && s.api.Logger != nil {
s.api.Logger(f, args...)
} else {
log.Printf(f, args...)
}
}
// Fatalf logs message either via defined user logger or via system one if no user logger is defined.
// Exits with non-zero status after printing
func (s *Server) Fatalf(f string, args ...interface{}) {
if s.api != nil && s.api.Logger != nil {
s.api.Logger(f, args...)
os.Exit(1)
}
log.Fatalf(f, args...)
}
// SetAPI configures the server with the specified API. Needs to be called before Serve
func (s *Server) SetAPI(api *operations.ConsoleAPI) {
if api == nil {
s.api = nil
s.handler = nil
return
}
s.api = api
s.handler = configureAPI(api)
}
func (s *Server) hasScheme(scheme string) bool {
schemes := s.EnabledListeners
if len(schemes) == 0 {
schemes = defaultSchemes
}
for _, v := range schemes {
if v == scheme {
return true
}
}
return false
}
// Serve the api
func (s *Server) Serve() (err error) {
if !s.hasListeners {
if err = s.Listen(); err != nil {
return err
}
}
// set default handler, if none is set
if s.handler == nil {
if s.api == nil {
return errors.New("can't create the default handler, as no api is set")
}
s.SetHandler(s.api.Serve(nil))
}
wg := new(sync.WaitGroup)
once := new(sync.Once)
signalNotify(s.interrupt)
go handleInterrupt(once, s)
servers := []*http.Server{}
if s.hasScheme(schemeUnix) {
domainSocket := new(http.Server)
domainSocket.MaxHeaderBytes = int(s.MaxHeaderSize)
domainSocket.Handler = s.handler
if int64(s.CleanupTimeout) > 0 {
domainSocket.IdleTimeout = s.CleanupTimeout
}
configureServer(domainSocket, "unix", string(s.SocketPath))
servers = append(servers, domainSocket)
wg.Add(1)
s.Logf("Serving console at unix://%s", s.SocketPath)
go func(l net.Listener) {
defer wg.Done()
if err := domainSocket.Serve(l); err != nil && err != http.ErrServerClosed {
s.Fatalf("%v", err)
}
s.Logf("Stopped serving console at unix://%s", s.SocketPath)
}(s.domainSocketL)
}
if s.hasScheme(schemeHTTP) {
httpServer := new(http.Server)
httpServer.MaxHeaderBytes = int(s.MaxHeaderSize)
httpServer.ReadTimeout = s.ReadTimeout
httpServer.WriteTimeout = s.WriteTimeout
httpServer.SetKeepAlivesEnabled(int64(s.KeepAlive) > 0)
if s.ListenLimit > 0 {
for i := range s.httpServerL {
s.httpServerL[i] = netutil.LimitListener(s.httpServerL[i], s.ListenLimit)
}
}
if int64(s.CleanupTimeout) > 0 {
httpServer.IdleTimeout = s.CleanupTimeout
}
httpServer.Handler = s.handler
configureServer(httpServer, "http", s.httpServerL[0].Addr().String())
servers = append(servers, httpServer)
s.Logf("Serving console at http://%s", s.httpServerL[0].Addr())
for i := range s.httpServerL {
wg.Add(1)
go func(l net.Listener) {
defer wg.Done()
if err := httpServer.Serve(l); err != nil && err != http.ErrServerClosed {
s.Fatalf("%v", err)
}
s.Logf("Stopped serving console at http://%s", l.Addr())
}(s.httpServerL[i])
}
}
if s.hasScheme(schemeHTTPS) {
httpsServer := new(http.Server)
httpsServer.MaxHeaderBytes = int(s.MaxHeaderSize)
httpsServer.ReadTimeout = s.TLSReadTimeout
httpsServer.WriteTimeout = s.TLSWriteTimeout
httpsServer.SetKeepAlivesEnabled(int64(s.TLSKeepAlive) > 0)
if s.TLSListenLimit > 0 {
for i := range s.httpsServerL {
s.httpsServerL[i] = netutil.LimitListener(s.httpsServerL[i], s.TLSListenLimit)
}
}
if int64(s.CleanupTimeout) > 0 {
httpsServer.IdleTimeout = s.CleanupTimeout
}
httpsServer.Handler = s.handler
// Inspired by https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
httpsServer.TLSConfig = &tls.Config{
// Causes servers to use Go's default ciphersuite preferences,
// which are tuned to avoid attacks. Does nothing on clients.
PreferServerCipherSuites: true,
// Only use curves which have assembly implementations
// https://github.com/golang/go/tree/master/src/crypto/elliptic
CurvePreferences: []tls.CurveID{tls.CurveP256},
// Use modern tls mode https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
NextProtos: []string{"h2", "http/1.1"},
// https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet#Rule_-_Only_Support_Strong_Protocols
MinVersion: tls.VersionTLS12,
// These ciphersuites support Forward Secrecy: https://en.wikipedia.org/wiki/Forward_secrecy
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
// build standard config from server options
if s.TLSCertificate != "" && s.TLSCertificateKey != "" {
httpsServer.TLSConfig.Certificates = make([]tls.Certificate, 1)
httpsServer.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(string(s.TLSCertificate), string(s.TLSCertificateKey))
if err != nil {
return err
}
}
if s.TLSCACertificate != "" {
// include specified CA certificate
caCert, caCertErr := os.ReadFile(string(s.TLSCACertificate))
if caCertErr != nil {
return caCertErr
}
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(caCert)
if !ok {
return fmt.Errorf("cannot parse CA certificate")
}
httpsServer.TLSConfig.ClientCAs = caCertPool
httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
// call custom TLS configurator
configureTLS(httpsServer.TLSConfig)
if len(httpsServer.TLSConfig.Certificates) == 0 && httpsServer.TLSConfig.GetCertificate == nil {
// after standard and custom config are passed, this ends up with no certificate
if s.TLSCertificate == "" {
if s.TLSCertificateKey == "" {
s.Fatalf("the required flags `--tls-certificate` and `--tls-key` were not specified")
}
s.Fatalf("the required flag `--tls-certificate` was not specified")
}
if s.TLSCertificateKey == "" {
s.Fatalf("the required flag `--tls-key` was not specified")
}
// this happens with a wrong custom TLS configurator
s.Fatalf("no certificate was configured for TLS")
}
configureServer(httpsServer, "https", s.httpsServerL[0].Addr().String())
servers = append(servers, httpsServer)
s.Logf("Serving console at https://%s", s.httpsServerL[0].Addr())
for i := range s.httpsServerL {
wg.Add(1)
go func(l net.Listener) {
defer wg.Done()
if err := httpsServer.Serve(l); err != nil && err != http.ErrServerClosed {
s.Fatalf("%v", err)
}
s.Logf("Stopped serving console at https://%s", l.Addr())
}(tls.NewListener(s.httpsServerL[i], httpsServer.TLSConfig))
}
}
wg.Add(1)
go s.handleShutdown(wg, &servers)
wg.Wait()
return nil
}
// Listen creates the listeners for the server
func (s *Server) Listen() error {
if s.hasListeners { // already done this
return nil
}
if s.hasScheme(schemeHTTPS) {
// Use http host if https host wasn't defined
if s.TLSHost == "" {
s.TLSHost = s.Host
}
// Use http listen limit if https listen limit wasn't defined
if s.TLSListenLimit == 0 {
s.TLSListenLimit = s.ListenLimit
}
// Use http tcp keep alive if https tcp keep alive wasn't defined
if int64(s.TLSKeepAlive) == 0 {
s.TLSKeepAlive = s.KeepAlive
}
// Use http read timeout if https read timeout wasn't defined
if int64(s.TLSReadTimeout) == 0 {
s.TLSReadTimeout = s.ReadTimeout
}
// Use http write timeout if https write timeout wasn't defined
if int64(s.TLSWriteTimeout) == 0 {
s.TLSWriteTimeout = s.WriteTimeout
}
}
if s.hasScheme(schemeUnix) {
domSockListener, err := net.Listen("unix", string(s.SocketPath))
if err != nil {
return err
}
s.domainSocketL = domSockListener
}
lookup := func(addr string) []net.IP {
ips, err := net.LookupIP(addr)
if err == nil {
return ips
}
return []net.IP{net.ParseIP(addr)}
}
convert := func(ip net.IP) (string, string) {
if ip == nil {
return "", "tcp"
}
proto := "tcp4"
if ip.To4() == nil {
proto = "tcp6"
}
return ip.String(), proto
}
if s.hasScheme(schemeHTTP) {
for _, ip := range lookup(s.Host) {
host, proto := convert(ip)
listener, err := net.Listen(proto, net.JoinHostPort(host, strconv.Itoa(s.Port)))
if err != nil {
return err
}
if s.Host == "" || s.Port == 0 {
h, p, err := swag.SplitHostPort(listener.Addr().String())
if err != nil {
return err
}
s.Host = h
s.Port = p
}
s.httpServerL = append(s.httpServerL, listener)
}
}
if s.hasScheme(schemeHTTPS) {
for _, ip := range lookup(s.TLSHost) {
host, proto := convert(ip)
tlsListener, err := net.Listen(proto, net.JoinHostPort(host, strconv.Itoa(s.TLSPort)))
if err != nil {
return err
}
if s.TLSHost == "" || s.TLSPort == 0 {
sh, sp, err := swag.SplitHostPort(tlsListener.Addr().String())
if err != nil {
return err
}
s.TLSHost = sh
s.TLSPort = sp
}
s.httpsServerL = append(s.httpsServerL, tlsListener)
}
}
s.hasListeners = true
return nil
}
// Shutdown server and clean up resources
func (s *Server) Shutdown() error {
if atomic.CompareAndSwapInt32(&s.shuttingDown, 0, 1) {
close(s.shutdown)
}
return nil
}
func (s *Server) handleShutdown(wg *sync.WaitGroup, serversPtr *[]*http.Server) {
// wg.Done must occur last, after s.api.ServerShutdown()
// (to preserve old behavior)
defer wg.Done()
<-s.shutdown
servers := *serversPtr
ctx, cancel := context.WithTimeout(context.TODO(), s.GracefulTimeout)
defer cancel()
// first execute the pre-shutdown hook
s.api.PreServerShutdown()
shutdownChan := make(chan bool)
for i := range servers {
server := servers[i]
go func() {
var success bool
defer func() {
shutdownChan <- success
}()
if err := server.Shutdown(ctx); err != nil {
// Error from closing listeners, or context timeout:
s.Logf("HTTP server Shutdown: %v", err)
} else {
success = true
}
}()
}
// Wait until all listeners have successfully shut down before calling ServerShutdown
success := true
for range servers {
success = success && <-shutdownChan
}
if success {
s.api.ServerShutdown()
}
}
// GetHandler returns a handler useful for testing
func (s *Server) GetHandler() http.Handler {
return s.handler
}
// SetHandler allows for setting a http handler on this server
func (s *Server) SetHandler(handler http.Handler) {
s.handler = handler
}
// UnixListener returns the domain socket listener
func (s *Server) UnixListener() (net.Listener, error) {
if !s.hasListeners {
if err := s.Listen(); err != nil {
return nil, err
}
}
return s.domainSocketL, nil
}
// HTTPListener returns the http listener
func (s *Server) HTTPListener() ([]net.Listener, error) {
if !s.hasListeners {
if err := s.Listen(); err != nil {
return nil, err
}
}
return s.httpServerL, nil
}
// TLSListener returns the https listener
func (s *Server) TLSListener() ([]net.Listener, error) {
if !s.hasListeners {
if err := s.Listen(); err != nil {
return nil, err
}
}
return s.httpsServerL, nil
}
func handleInterrupt(once *sync.Once, s *Server) {
once.Do(func() {
for range s.interrupt {
if s.interrupted {
s.Logf("Server already shutting down")
continue
}
s.interrupted = true
s.Logf("Shutting down... ")
if err := s.Shutdown(); err != nil {
s.Logf("HTTP server Shutdown: %v", err)
}
}
})
}
func signalNotify(interrupt chan<- os.Signal) {
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,284 +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")
ErrPolicyNameContainsSpace = errors.New("error policy name cannot contain spaces")
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()
}
if errors.Is(err1, ErrPolicyNameContainsSpace) {
errorCode = 400
errorMessage = ErrPolicyNameContainsSpace.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,159 +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},
"ErrBucketLifeCycleNotConfigured": {code: 404, err: ErrBucketLifeCycleNotConfigured},
"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,81 +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 (
"net/http"
"os"
"github.com/minio/pkg/v3/licverifier"
"github.com/minio/pkg/v3/subnet"
)
type SubnetPlan int
const (
PlanAGPL SubnetPlan = iota
PlanStandard
PlanEnterprise
PlanEnterpriseLite
PlanEnterprisePlus
)
func (sp SubnetPlan) String() string {
switch sp {
case PlanStandard:
return "standard"
case PlanEnterprise:
return "enterprise"
case PlanEnterpriseLite:
return "enterprise-lite"
case PlanEnterprisePlus:
return "enterprise-plus"
default:
return "agpl"
}
}
var InstanceLicensePlan = PlanAGPL
func getLicenseInfo(client http.Client, license string) (*licverifier.LicenseInfo, error) {
lv := subnet.LicenseValidator{
Client: client,
ExpiryGracePeriod: 0,
}
lv.Init(getConsoleDevMode())
return lv.ParseLicense(license)
}
func fetchLicensePlan() {
client := GetConsoleHTTPClient("127.0.0.1")
licenseInfo, err := getLicenseInfo(*client, os.Getenv(EnvSubnetLicense))
if err != nil {
return
}
switch licenseInfo.Plan {
case "STANDARD":
InstanceLicensePlan = PlanStandard
case "ENTERPRISE":
InstanceLicensePlan = PlanEnterprise
case "ENTERPRISE-LITE":
InstanceLicensePlan = PlanEnterpriseLite
case "ENTERPRISE-PLUS":
InstanceLicensePlan = PlanEnterprisePlus
default:
InstanceLicensePlan = PlanAGPL
}
}

View File

@@ -1,83 +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"
"log"
"os"
"github.com/minio/cli"
)
var (
infoLog = log.New(os.Stdout, "I: ", log.LstdFlags)
errorLog = log.New(os.Stdout, "E: ", log.LstdFlags)
)
func logInfo(msg string, data ...interface{}) {
infoLog.Printf(msg+"\n", data...)
}
func logError(msg string, data ...interface{}) {
errorLog.Printf(msg+"\n", data...)
}
func logIf(_ context.Context, _ error, _ ...interface{}) {
}
// globally changeable logger styles
var (
LogInfo = logInfo
LogError = logError
LogIf = logIf
)
// Context captures all command line flags values
type Context struct {
Host string
HTTPPort, HTTPSPort int
TLSRedirect string
// Legacy options, TODO: remove in future
TLSCertificate, TLSKey, TLSca string
}
// Load loads api Context from command line context.
func (c *Context) Load(ctx *cli.Context) error {
*c = Context{
Host: ctx.String("host"),
HTTPPort: ctx.Int("port"),
HTTPSPort: ctx.Int("tls-port"),
TLSRedirect: ctx.String("tls-redirect"),
// Legacy options to be removed.
TLSCertificate: ctx.String("tls-certificate"),
TLSKey: ctx.String("tls-key"),
TLSca: ctx.String("tls-ca"),
}
if c.HTTPPort > 65535 {
return errors.New("invalid argument --port out of range - ports can range from 1-65535")
}
if c.HTTPSPort > 65535 {
return errors.New("invalid argument --tls-port out of range - ports can range from 1-65535")
}
if c.TLSRedirect != "on" && c.TLSRedirect != "off" {
return errors.New("invalid argument --tls-redirect only accepts either 'on' or 'off'")
}
return nil
}

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,88 +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 account
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// AccountChangePasswordHandlerFunc turns a function with the right signature into a account change password handler
type AccountChangePasswordHandlerFunc func(AccountChangePasswordParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn AccountChangePasswordHandlerFunc) Handle(params AccountChangePasswordParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// AccountChangePasswordHandler interface for that can handle valid account change password params
type AccountChangePasswordHandler interface {
Handle(AccountChangePasswordParams, *models.Principal) middleware.Responder
}
// NewAccountChangePassword creates a new http.Handler for the account change password operation
func NewAccountChangePassword(ctx *middleware.Context, handler AccountChangePasswordHandler) *AccountChangePassword {
return &AccountChangePassword{Context: ctx, Handler: handler}
}
/*
AccountChangePassword swagger:route POST /account/change-password Account accountChangePassword
Change password of currently logged in user.
*/
type AccountChangePassword struct {
Context *middleware.Context
Handler AccountChangePasswordHandler
}
func (o *AccountChangePassword) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewAccountChangePasswordParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,101 +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 account
// 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/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewAccountChangePasswordParams creates a new AccountChangePasswordParams object
//
// There are no default values defined in the spec.
func NewAccountChangePasswordParams() AccountChangePasswordParams {
return AccountChangePasswordParams{}
}
// AccountChangePasswordParams contains all the bound params for the account change password operation
// typically these are obtained from a http.Request
//
// swagger:parameters AccountChangePassword
type AccountChangePasswordParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.AccountChangePasswordRequest
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewAccountChangePasswordParams() beforehand.
func (o *AccountChangePasswordParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.AccountChangePasswordRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,115 +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 account
// 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"
)
// AccountChangePasswordNoContentCode is the HTTP code returned for type AccountChangePasswordNoContent
const AccountChangePasswordNoContentCode int = 204
/*
AccountChangePasswordNoContent A successful login.
swagger:response accountChangePasswordNoContent
*/
type AccountChangePasswordNoContent struct {
}
// NewAccountChangePasswordNoContent creates AccountChangePasswordNoContent with default headers values
func NewAccountChangePasswordNoContent() *AccountChangePasswordNoContent {
return &AccountChangePasswordNoContent{}
}
// WriteResponse to the client
func (o *AccountChangePasswordNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(204)
}
/*
AccountChangePasswordDefault Generic error response.
swagger:response accountChangePasswordDefault
*/
type AccountChangePasswordDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewAccountChangePasswordDefault creates AccountChangePasswordDefault with default headers values
func NewAccountChangePasswordDefault(code int) *AccountChangePasswordDefault {
if code <= 0 {
code = 500
}
return &AccountChangePasswordDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the account change password default response
func (o *AccountChangePasswordDefault) WithStatusCode(code int) *AccountChangePasswordDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the account change password default response
func (o *AccountChangePasswordDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the account change password default response
func (o *AccountChangePasswordDefault) WithPayload(payload *models.APIError) *AccountChangePasswordDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the account change password default response
func (o *AccountChangePasswordDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *AccountChangePasswordDefault) 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,104 +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 account
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// AccountChangePasswordURL generates an URL for the account change password operation
type AccountChangePasswordURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AccountChangePasswordURL) WithBasePath(bp string) *AccountChangePasswordURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AccountChangePasswordURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *AccountChangePasswordURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/account/change-password"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *AccountChangePasswordURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *AccountChangePasswordURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *AccountChangePasswordURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on AccountChangePasswordURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on AccountChangePasswordURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *AccountChangePasswordURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,88 +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 account
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// ChangeUserPasswordHandlerFunc turns a function with the right signature into a change user password handler
type ChangeUserPasswordHandlerFunc func(ChangeUserPasswordParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn ChangeUserPasswordHandlerFunc) Handle(params ChangeUserPasswordParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// ChangeUserPasswordHandler interface for that can handle valid change user password params
type ChangeUserPasswordHandler interface {
Handle(ChangeUserPasswordParams, *models.Principal) middleware.Responder
}
// NewChangeUserPassword creates a new http.Handler for the change user password operation
func NewChangeUserPassword(ctx *middleware.Context, handler ChangeUserPasswordHandler) *ChangeUserPassword {
return &ChangeUserPassword{Context: ctx, Handler: handler}
}
/*
ChangeUserPassword swagger:route POST /account/change-user-password Account changeUserPassword
Change password of currently logged in user.
*/
type ChangeUserPassword struct {
Context *middleware.Context
Handler ChangeUserPasswordHandler
}
func (o *ChangeUserPassword) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewChangeUserPasswordParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,101 +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 account
// 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/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewChangeUserPasswordParams creates a new ChangeUserPasswordParams object
//
// There are no default values defined in the spec.
func NewChangeUserPasswordParams() ChangeUserPasswordParams {
return ChangeUserPasswordParams{}
}
// ChangeUserPasswordParams contains all the bound params for the change user password operation
// typically these are obtained from a http.Request
//
// swagger:parameters ChangeUserPassword
type ChangeUserPasswordParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.ChangeUserPasswordRequest
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewChangeUserPasswordParams() beforehand.
func (o *ChangeUserPasswordParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.ChangeUserPasswordRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,115 +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 account
// 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"
)
// ChangeUserPasswordCreatedCode is the HTTP code returned for type ChangeUserPasswordCreated
const ChangeUserPasswordCreatedCode int = 201
/*
ChangeUserPasswordCreated Password successfully changed.
swagger:response changeUserPasswordCreated
*/
type ChangeUserPasswordCreated struct {
}
// NewChangeUserPasswordCreated creates ChangeUserPasswordCreated with default headers values
func NewChangeUserPasswordCreated() *ChangeUserPasswordCreated {
return &ChangeUserPasswordCreated{}
}
// WriteResponse to the client
func (o *ChangeUserPasswordCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(201)
}
/*
ChangeUserPasswordDefault Generic error response.
swagger:response changeUserPasswordDefault
*/
type ChangeUserPasswordDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewChangeUserPasswordDefault creates ChangeUserPasswordDefault with default headers values
func NewChangeUserPasswordDefault(code int) *ChangeUserPasswordDefault {
if code <= 0 {
code = 500
}
return &ChangeUserPasswordDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the change user password default response
func (o *ChangeUserPasswordDefault) WithStatusCode(code int) *ChangeUserPasswordDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the change user password default response
func (o *ChangeUserPasswordDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the change user password default response
func (o *ChangeUserPasswordDefault) WithPayload(payload *models.APIError) *ChangeUserPasswordDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the change user password default response
func (o *ChangeUserPasswordDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ChangeUserPasswordDefault) 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,104 +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 account
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// ChangeUserPasswordURL generates an URL for the change user password operation
type ChangeUserPasswordURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *ChangeUserPasswordURL) WithBasePath(bp string) *ChangeUserPasswordURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *ChangeUserPasswordURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *ChangeUserPasswordURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/account/change-user-password"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *ChangeUserPasswordURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *ChangeUserPasswordURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *ChangeUserPasswordURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on ChangeUserPasswordURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on ChangeUserPasswordURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *ChangeUserPasswordURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,88 +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 generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// AddBucketLifecycleHandlerFunc turns a function with the right signature into a add bucket lifecycle handler
type AddBucketLifecycleHandlerFunc func(AddBucketLifecycleParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn AddBucketLifecycleHandlerFunc) Handle(params AddBucketLifecycleParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// AddBucketLifecycleHandler interface for that can handle valid add bucket lifecycle params
type AddBucketLifecycleHandler interface {
Handle(AddBucketLifecycleParams, *models.Principal) middleware.Responder
}
// NewAddBucketLifecycle creates a new http.Handler for the add bucket lifecycle operation
func NewAddBucketLifecycle(ctx *middleware.Context, handler AddBucketLifecycleHandler) *AddBucketLifecycle {
return &AddBucketLifecycle{Context: ctx, Handler: handler}
}
/*
AddBucketLifecycle swagger:route POST /buckets/{bucket_name}/lifecycle Bucket addBucketLifecycle
Add Bucket Lifecycle
*/
type AddBucketLifecycle struct {
Context *middleware.Context
Handler AddBucketLifecycleHandler
}
func (o *AddBucketLifecycle) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewAddBucketLifecycleParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,126 +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 (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewAddBucketLifecycleParams creates a new AddBucketLifecycleParams object
//
// There are no default values defined in the spec.
func NewAddBucketLifecycleParams() AddBucketLifecycleParams {
return AddBucketLifecycleParams{}
}
// AddBucketLifecycleParams contains all the bound params for the add bucket lifecycle operation
// typically these are obtained from a http.Request
//
// swagger:parameters AddBucketLifecycle
type AddBucketLifecycleParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.AddBucketLifecycle
/*
Required: true
In: path
*/
BucketName string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewAddBucketLifecycleParams() beforehand.
func (o *AddBucketLifecycleParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.AddBucketLifecycle
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindBucketName binds and validates parameter BucketName from path.
func (o *AddBucketLifecycleParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.BucketName = raw
return nil
}

View File

@@ -1,115 +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"
)
// AddBucketLifecycleCreatedCode is the HTTP code returned for type AddBucketLifecycleCreated
const AddBucketLifecycleCreatedCode int = 201
/*
AddBucketLifecycleCreated A successful response.
swagger:response addBucketLifecycleCreated
*/
type AddBucketLifecycleCreated struct {
}
// NewAddBucketLifecycleCreated creates AddBucketLifecycleCreated with default headers values
func NewAddBucketLifecycleCreated() *AddBucketLifecycleCreated {
return &AddBucketLifecycleCreated{}
}
// WriteResponse to the client
func (o *AddBucketLifecycleCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(201)
}
/*
AddBucketLifecycleDefault Generic error response.
swagger:response addBucketLifecycleDefault
*/
type AddBucketLifecycleDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewAddBucketLifecycleDefault creates AddBucketLifecycleDefault with default headers values
func NewAddBucketLifecycleDefault(code int) *AddBucketLifecycleDefault {
if code <= 0 {
code = 500
}
return &AddBucketLifecycleDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the add bucket lifecycle default response
func (o *AddBucketLifecycleDefault) WithStatusCode(code int) *AddBucketLifecycleDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the add bucket lifecycle default response
func (o *AddBucketLifecycleDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the add bucket lifecycle default response
func (o *AddBucketLifecycleDefault) WithPayload(payload *models.APIError) *AddBucketLifecycleDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the add bucket lifecycle default response
func (o *AddBucketLifecycleDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *AddBucketLifecycleDefault) 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,116 +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 generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// AddBucketLifecycleURL generates an URL for the add bucket lifecycle operation
type AddBucketLifecycleURL struct {
BucketName string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AddBucketLifecycleURL) WithBasePath(bp string) *AddBucketLifecycleURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AddBucketLifecycleURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *AddBucketLifecycleURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/buckets/{bucket_name}/lifecycle"
bucketName := o.BucketName
if bucketName != "" {
_path = strings.Replace(_path, "{bucket_name}", bucketName, -1)
} else {
return nil, errors.New("bucketName is required on AddBucketLifecycleURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *AddBucketLifecycleURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *AddBucketLifecycleURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *AddBucketLifecycleURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on AddBucketLifecycleURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on AddBucketLifecycleURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *AddBucketLifecycleURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,88 +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 generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// AddMultiBucketLifecycleHandlerFunc turns a function with the right signature into a add multi bucket lifecycle handler
type AddMultiBucketLifecycleHandlerFunc func(AddMultiBucketLifecycleParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn AddMultiBucketLifecycleHandlerFunc) Handle(params AddMultiBucketLifecycleParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// AddMultiBucketLifecycleHandler interface for that can handle valid add multi bucket lifecycle params
type AddMultiBucketLifecycleHandler interface {
Handle(AddMultiBucketLifecycleParams, *models.Principal) middleware.Responder
}
// NewAddMultiBucketLifecycle creates a new http.Handler for the add multi bucket lifecycle operation
func NewAddMultiBucketLifecycle(ctx *middleware.Context, handler AddMultiBucketLifecycleHandler) *AddMultiBucketLifecycle {
return &AddMultiBucketLifecycle{Context: ctx, Handler: handler}
}
/*
AddMultiBucketLifecycle swagger:route POST /buckets/multi-lifecycle Bucket addMultiBucketLifecycle
Add Multi Bucket Lifecycle
*/
type AddMultiBucketLifecycle struct {
Context *middleware.Context
Handler AddMultiBucketLifecycleHandler
}
func (o *AddMultiBucketLifecycle) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewAddMultiBucketLifecycleParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,101 +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 (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewAddMultiBucketLifecycleParams creates a new AddMultiBucketLifecycleParams object
//
// There are no default values defined in the spec.
func NewAddMultiBucketLifecycleParams() AddMultiBucketLifecycleParams {
return AddMultiBucketLifecycleParams{}
}
// AddMultiBucketLifecycleParams contains all the bound params for the add multi bucket lifecycle operation
// typically these are obtained from a http.Request
//
// swagger:parameters AddMultiBucketLifecycle
type AddMultiBucketLifecycleParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.AddMultiBucketLifecycle
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewAddMultiBucketLifecycleParams() beforehand.
func (o *AddMultiBucketLifecycleParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.AddMultiBucketLifecycle
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return 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"
)
// AddMultiBucketLifecycleOKCode is the HTTP code returned for type AddMultiBucketLifecycleOK
const AddMultiBucketLifecycleOKCode int = 200
/*
AddMultiBucketLifecycleOK A successful response.
swagger:response addMultiBucketLifecycleOK
*/
type AddMultiBucketLifecycleOK struct {
/*
In: Body
*/
Payload *models.MultiLifecycleResult `json:"body,omitempty"`
}
// NewAddMultiBucketLifecycleOK creates AddMultiBucketLifecycleOK with default headers values
func NewAddMultiBucketLifecycleOK() *AddMultiBucketLifecycleOK {
return &AddMultiBucketLifecycleOK{}
}
// WithPayload adds the payload to the add multi bucket lifecycle o k response
func (o *AddMultiBucketLifecycleOK) WithPayload(payload *models.MultiLifecycleResult) *AddMultiBucketLifecycleOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the add multi bucket lifecycle o k response
func (o *AddMultiBucketLifecycleOK) SetPayload(payload *models.MultiLifecycleResult) {
o.Payload = payload
}
// WriteResponse to the client
func (o *AddMultiBucketLifecycleOK) 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
}
}
}
/*
AddMultiBucketLifecycleDefault Generic error response.
swagger:response addMultiBucketLifecycleDefault
*/
type AddMultiBucketLifecycleDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewAddMultiBucketLifecycleDefault creates AddMultiBucketLifecycleDefault with default headers values
func NewAddMultiBucketLifecycleDefault(code int) *AddMultiBucketLifecycleDefault {
if code <= 0 {
code = 500
}
return &AddMultiBucketLifecycleDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the add multi bucket lifecycle default response
func (o *AddMultiBucketLifecycleDefault) WithStatusCode(code int) *AddMultiBucketLifecycleDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the add multi bucket lifecycle default response
func (o *AddMultiBucketLifecycleDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the add multi bucket lifecycle default response
func (o *AddMultiBucketLifecycleDefault) WithPayload(payload *models.APIError) *AddMultiBucketLifecycleDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the add multi bucket lifecycle default response
func (o *AddMultiBucketLifecycleDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *AddMultiBucketLifecycleDefault) 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,104 +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 generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// AddMultiBucketLifecycleURL generates an URL for the add multi bucket lifecycle operation
type AddMultiBucketLifecycleURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AddMultiBucketLifecycleURL) WithBasePath(bp string) *AddMultiBucketLifecycleURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AddMultiBucketLifecycleURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *AddMultiBucketLifecycleURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/buckets/multi-lifecycle"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *AddMultiBucketLifecycleURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *AddMultiBucketLifecycleURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *AddMultiBucketLifecycleURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on AddMultiBucketLifecycleURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on AddMultiBucketLifecycleURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *AddMultiBucketLifecycleURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,88 +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 generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// AddRemoteBucketHandlerFunc turns a function with the right signature into a add remote bucket handler
type AddRemoteBucketHandlerFunc func(AddRemoteBucketParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn AddRemoteBucketHandlerFunc) Handle(params AddRemoteBucketParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// AddRemoteBucketHandler interface for that can handle valid add remote bucket params
type AddRemoteBucketHandler interface {
Handle(AddRemoteBucketParams, *models.Principal) middleware.Responder
}
// NewAddRemoteBucket creates a new http.Handler for the add remote bucket operation
func NewAddRemoteBucket(ctx *middleware.Context, handler AddRemoteBucketHandler) *AddRemoteBucket {
return &AddRemoteBucket{Context: ctx, Handler: handler}
}
/*
AddRemoteBucket swagger:route POST /remote-buckets Bucket addRemoteBucket
Add Remote Bucket
*/
type AddRemoteBucket struct {
Context *middleware.Context
Handler AddRemoteBucketHandler
}
func (o *AddRemoteBucket) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewAddRemoteBucketParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,101 +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 (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewAddRemoteBucketParams creates a new AddRemoteBucketParams object
//
// There are no default values defined in the spec.
func NewAddRemoteBucketParams() AddRemoteBucketParams {
return AddRemoteBucketParams{}
}
// AddRemoteBucketParams contains all the bound params for the add remote bucket operation
// typically these are obtained from a http.Request
//
// swagger:parameters AddRemoteBucket
type AddRemoteBucketParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.CreateRemoteBucket
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewAddRemoteBucketParams() beforehand.
func (o *AddRemoteBucketParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.CreateRemoteBucket
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,115 +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"
)
// AddRemoteBucketCreatedCode is the HTTP code returned for type AddRemoteBucketCreated
const AddRemoteBucketCreatedCode int = 201
/*
AddRemoteBucketCreated A successful response.
swagger:response addRemoteBucketCreated
*/
type AddRemoteBucketCreated struct {
}
// NewAddRemoteBucketCreated creates AddRemoteBucketCreated with default headers values
func NewAddRemoteBucketCreated() *AddRemoteBucketCreated {
return &AddRemoteBucketCreated{}
}
// WriteResponse to the client
func (o *AddRemoteBucketCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(201)
}
/*
AddRemoteBucketDefault Generic error response.
swagger:response addRemoteBucketDefault
*/
type AddRemoteBucketDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewAddRemoteBucketDefault creates AddRemoteBucketDefault with default headers values
func NewAddRemoteBucketDefault(code int) *AddRemoteBucketDefault {
if code <= 0 {
code = 500
}
return &AddRemoteBucketDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the add remote bucket default response
func (o *AddRemoteBucketDefault) WithStatusCode(code int) *AddRemoteBucketDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the add remote bucket default response
func (o *AddRemoteBucketDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the add remote bucket default response
func (o *AddRemoteBucketDefault) WithPayload(payload *models.APIError) *AddRemoteBucketDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the add remote bucket default response
func (o *AddRemoteBucketDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *AddRemoteBucketDefault) 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,104 +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 generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// AddRemoteBucketURL generates an URL for the add remote bucket operation
type AddRemoteBucketURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AddRemoteBucketURL) WithBasePath(bp string) *AddRemoteBucketURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *AddRemoteBucketURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *AddRemoteBucketURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/remote-buckets"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *AddRemoteBucketURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *AddRemoteBucketURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *AddRemoteBucketURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on AddRemoteBucketURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on AddRemoteBucketURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *AddRemoteBucketURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,88 +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 generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// DeleteAccessRuleWithBucketHandlerFunc turns a function with the right signature into a delete access rule with bucket handler
type DeleteAccessRuleWithBucketHandlerFunc func(DeleteAccessRuleWithBucketParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn DeleteAccessRuleWithBucketHandlerFunc) Handle(params DeleteAccessRuleWithBucketParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// DeleteAccessRuleWithBucketHandler interface for that can handle valid delete access rule with bucket params
type DeleteAccessRuleWithBucketHandler interface {
Handle(DeleteAccessRuleWithBucketParams, *models.Principal) middleware.Responder
}
// NewDeleteAccessRuleWithBucket creates a new http.Handler for the delete access rule with bucket operation
func NewDeleteAccessRuleWithBucket(ctx *middleware.Context, handler DeleteAccessRuleWithBucketHandler) *DeleteAccessRuleWithBucket {
return &DeleteAccessRuleWithBucket{Context: ctx, Handler: handler}
}
/*
DeleteAccessRuleWithBucket swagger:route DELETE /bucket/{bucket}/access-rules Bucket deleteAccessRuleWithBucket
Delete Access Rule From Given Bucket
*/
type DeleteAccessRuleWithBucket struct {
Context *middleware.Context
Handler DeleteAccessRuleWithBucketHandler
}
func (o *DeleteAccessRuleWithBucket) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewDeleteAccessRuleWithBucketParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,126 +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 (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewDeleteAccessRuleWithBucketParams creates a new DeleteAccessRuleWithBucketParams object
//
// There are no default values defined in the spec.
func NewDeleteAccessRuleWithBucketParams() DeleteAccessRuleWithBucketParams {
return DeleteAccessRuleWithBucketParams{}
}
// DeleteAccessRuleWithBucketParams contains all the bound params for the delete access rule with bucket operation
// typically these are obtained from a http.Request
//
// swagger:parameters DeleteAccessRuleWithBucket
type DeleteAccessRuleWithBucketParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
Bucket string
/*
Required: true
In: body
*/
Prefix *models.PrefixWrapper
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewDeleteAccessRuleWithBucketParams() beforehand.
func (o *DeleteAccessRuleWithBucketParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rBucket, rhkBucket, _ := route.Params.GetOK("bucket")
if err := o.bindBucket(rBucket, rhkBucket, route.Formats); err != nil {
res = append(res, err)
}
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.PrefixWrapper
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("prefix", "body", ""))
} else {
res = append(res, errors.NewParseError("prefix", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Prefix = &body
}
}
} else {
res = append(res, errors.Required("prefix", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindBucket binds and validates parameter Bucket from path.
func (o *DeleteAccessRuleWithBucketParams) bindBucket(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Bucket = raw
return nil
}

View File

@@ -1,133 +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"
)
// DeleteAccessRuleWithBucketOKCode is the HTTP code returned for type DeleteAccessRuleWithBucketOK
const DeleteAccessRuleWithBucketOKCode int = 200
/*
DeleteAccessRuleWithBucketOK A successful response.
swagger:response deleteAccessRuleWithBucketOK
*/
type DeleteAccessRuleWithBucketOK struct {
/*
In: Body
*/
Payload bool `json:"body,omitempty"`
}
// NewDeleteAccessRuleWithBucketOK creates DeleteAccessRuleWithBucketOK with default headers values
func NewDeleteAccessRuleWithBucketOK() *DeleteAccessRuleWithBucketOK {
return &DeleteAccessRuleWithBucketOK{}
}
// WithPayload adds the payload to the delete access rule with bucket o k response
func (o *DeleteAccessRuleWithBucketOK) WithPayload(payload bool) *DeleteAccessRuleWithBucketOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the delete access rule with bucket o k response
func (o *DeleteAccessRuleWithBucketOK) SetPayload(payload bool) {
o.Payload = payload
}
// WriteResponse to the client
func (o *DeleteAccessRuleWithBucketOK) 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
}
}
/*
DeleteAccessRuleWithBucketDefault Generic error response.
swagger:response deleteAccessRuleWithBucketDefault
*/
type DeleteAccessRuleWithBucketDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewDeleteAccessRuleWithBucketDefault creates DeleteAccessRuleWithBucketDefault with default headers values
func NewDeleteAccessRuleWithBucketDefault(code int) *DeleteAccessRuleWithBucketDefault {
if code <= 0 {
code = 500
}
return &DeleteAccessRuleWithBucketDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the delete access rule with bucket default response
func (o *DeleteAccessRuleWithBucketDefault) WithStatusCode(code int) *DeleteAccessRuleWithBucketDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the delete access rule with bucket default response
func (o *DeleteAccessRuleWithBucketDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the delete access rule with bucket default response
func (o *DeleteAccessRuleWithBucketDefault) WithPayload(payload *models.APIError) *DeleteAccessRuleWithBucketDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the delete access rule with bucket default response
func (o *DeleteAccessRuleWithBucketDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *DeleteAccessRuleWithBucketDefault) 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,88 +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 generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// DeleteAllReplicationRulesHandlerFunc turns a function with the right signature into a delete all replication rules handler
type DeleteAllReplicationRulesHandlerFunc func(DeleteAllReplicationRulesParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn DeleteAllReplicationRulesHandlerFunc) Handle(params DeleteAllReplicationRulesParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// DeleteAllReplicationRulesHandler interface for that can handle valid delete all replication rules params
type DeleteAllReplicationRulesHandler interface {
Handle(DeleteAllReplicationRulesParams, *models.Principal) middleware.Responder
}
// NewDeleteAllReplicationRules creates a new http.Handler for the delete all replication rules operation
func NewDeleteAllReplicationRules(ctx *middleware.Context, handler DeleteAllReplicationRulesHandler) *DeleteAllReplicationRules {
return &DeleteAllReplicationRules{Context: ctx, Handler: handler}
}
/*
DeleteAllReplicationRules swagger:route DELETE /buckets/{bucket_name}/delete-all-replication-rules Bucket deleteAllReplicationRules
Deletes all replication rules from a bucket
*/
type DeleteAllReplicationRules struct {
Context *middleware.Context
Handler DeleteAllReplicationRulesHandler
}
func (o *DeleteAllReplicationRules) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewDeleteAllReplicationRulesParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -1,88 +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/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewDeleteAllReplicationRulesParams creates a new DeleteAllReplicationRulesParams object
//
// There are no default values defined in the spec.
func NewDeleteAllReplicationRulesParams() DeleteAllReplicationRulesParams {
return DeleteAllReplicationRulesParams{}
}
// DeleteAllReplicationRulesParams contains all the bound params for the delete all replication rules operation
// typically these are obtained from a http.Request
//
// swagger:parameters DeleteAllReplicationRules
type DeleteAllReplicationRulesParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
BucketName string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewDeleteAllReplicationRulesParams() beforehand.
func (o *DeleteAllReplicationRulesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindBucketName binds and validates parameter BucketName from path.
func (o *DeleteAllReplicationRulesParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.BucketName = raw
return nil
}

View File

@@ -1,115 +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"
)
// DeleteAllReplicationRulesNoContentCode is the HTTP code returned for type DeleteAllReplicationRulesNoContent
const DeleteAllReplicationRulesNoContentCode int = 204
/*
DeleteAllReplicationRulesNoContent A successful response.
swagger:response deleteAllReplicationRulesNoContent
*/
type DeleteAllReplicationRulesNoContent struct {
}
// NewDeleteAllReplicationRulesNoContent creates DeleteAllReplicationRulesNoContent with default headers values
func NewDeleteAllReplicationRulesNoContent() *DeleteAllReplicationRulesNoContent {
return &DeleteAllReplicationRulesNoContent{}
}
// WriteResponse to the client
func (o *DeleteAllReplicationRulesNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(204)
}
/*
DeleteAllReplicationRulesDefault Generic error response.
swagger:response deleteAllReplicationRulesDefault
*/
type DeleteAllReplicationRulesDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.APIError `json:"body,omitempty"`
}
// NewDeleteAllReplicationRulesDefault creates DeleteAllReplicationRulesDefault with default headers values
func NewDeleteAllReplicationRulesDefault(code int) *DeleteAllReplicationRulesDefault {
if code <= 0 {
code = 500
}
return &DeleteAllReplicationRulesDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the delete all replication rules default response
func (o *DeleteAllReplicationRulesDefault) WithStatusCode(code int) *DeleteAllReplicationRulesDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the delete all replication rules default response
func (o *DeleteAllReplicationRulesDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the delete all replication rules default response
func (o *DeleteAllReplicationRulesDefault) WithPayload(payload *models.APIError) *DeleteAllReplicationRulesDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the delete all replication rules default response
func (o *DeleteAllReplicationRulesDefault) SetPayload(payload *models.APIError) {
o.Payload = payload
}
// WriteResponse to the client
func (o *DeleteAllReplicationRulesDefault) 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,116 +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 generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// DeleteAllReplicationRulesURL generates an URL for the delete all replication rules operation
type DeleteAllReplicationRulesURL struct {
BucketName string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *DeleteAllReplicationRulesURL) WithBasePath(bp string) *DeleteAllReplicationRulesURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *DeleteAllReplicationRulesURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *DeleteAllReplicationRulesURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/buckets/{bucket_name}/delete-all-replication-rules"
bucketName := o.BucketName
if bucketName != "" {
_path = strings.Replace(_path, "{bucket_name}", bucketName, -1)
} else {
return nil, errors.New("bucketName is required on DeleteAllReplicationRulesURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *DeleteAllReplicationRulesURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *DeleteAllReplicationRulesURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *DeleteAllReplicationRulesURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on DeleteAllReplicationRulesURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on DeleteAllReplicationRulesURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *DeleteAllReplicationRulesURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -1,88 +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 generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// DeleteBucketLifecycleRuleHandlerFunc turns a function with the right signature into a delete bucket lifecycle rule handler
type DeleteBucketLifecycleRuleHandlerFunc func(DeleteBucketLifecycleRuleParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn DeleteBucketLifecycleRuleHandlerFunc) Handle(params DeleteBucketLifecycleRuleParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// DeleteBucketLifecycleRuleHandler interface for that can handle valid delete bucket lifecycle rule params
type DeleteBucketLifecycleRuleHandler interface {
Handle(DeleteBucketLifecycleRuleParams, *models.Principal) middleware.Responder
}
// NewDeleteBucketLifecycleRule creates a new http.Handler for the delete bucket lifecycle rule operation
func NewDeleteBucketLifecycleRule(ctx *middleware.Context, handler DeleteBucketLifecycleRuleHandler) *DeleteBucketLifecycleRule {
return &DeleteBucketLifecycleRule{Context: ctx, Handler: handler}
}
/*
DeleteBucketLifecycleRule swagger:route DELETE /buckets/{bucket_name}/lifecycle/{lifecycle_id} Bucket deleteBucketLifecycleRule
Delete Lifecycle rule
*/
type DeleteBucketLifecycleRule struct {
Context *middleware.Context
Handler DeleteBucketLifecycleRuleHandler
}
func (o *DeleteBucketLifecycleRule) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewDeleteBucketLifecycleRuleParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

Some files were not shown because too many files have changed in this diff Show More