mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
feat: integrate openID auth logic and user profile management (#1381)
This change introduces OpenID authn by using providers such as Github, Gitlab, Google and Dex. User sessions are now used for web clients to identify and persist an authenticated users session, thus not requiring every request to use credentials. Another change is apikey feature, users can create/revoke their api keys and use them to authenticate when using cli clients such as skopeo. eg: login: /auth/login?provider=github /auth/login?provider=gitlab and so on logout: /auth/logout redirectURL: /auth/callback/github /auth/callback/gitlab and so on If network policy doesn't allow inbound connections, this callback wont work! for more info read documentation added in this commit. Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro> Signed-off-by: Petu Eusebiu <peusebiu@cisco.com> Co-authored-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
parent
5494a1b8d6
commit
17d1338af1
51 changed files with 5467 additions and 624 deletions
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
|||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOFLAGS: "-tags=sync,search,scrub,metrics,userprefs,containers_image_openpgp"
|
||||
GOFLAGS: "-tags=sync,search,scrub,metrics,userprefs,apikey,containers_image_openpgp"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
6
.github/workflows/ecosystem-tools.yaml
vendored
6
.github/workflows/ecosystem-tools.yaml
vendored
|
@ -46,6 +46,12 @@ jobs:
|
|||
sudo systemctl enable crio.service
|
||||
sudo systemctl start crio.service
|
||||
sudo chmod 0777 /var/run/crio/crio.sock
|
||||
# install dex
|
||||
git clone https://github.com/dexidp/dex.git
|
||||
cd dex/
|
||||
make bin/dex
|
||||
./bin/dex serve $GITHUB_WORKSPACE/test/dex/config-dev.yaml &
|
||||
cd $GITHUB_WORKSPACE
|
||||
- name: Run referrers tests
|
||||
run: |
|
||||
make test-bats-referrers
|
||||
|
|
2
.github/workflows/golangci-lint.yaml
vendored
2
.github/workflows/golangci-lint.yaml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,userprefs,metrics,containers_image_openpgp,lint,mgmt ./cmd/... ./pkg/...
|
||||
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,userprefs,metrics,containers_image_openpgp,lint,mgmt,apikey ./cmd/... ./pkg/...
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
|
4
Makefile
4
Makefile
|
@ -32,8 +32,8 @@ TESTDATA := $(TOP_LEVEL)/test/data
|
|||
OS ?= linux
|
||||
ARCH ?= amd64
|
||||
BENCH_OUTPUT ?= stdout
|
||||
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs
|
||||
UI_DEPENDENCIES := search,mgmt,userprefs
|
||||
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,apikey
|
||||
UI_DEPENDENCIES := search,mgmt,userprefs,apikey
|
||||
comma:= ,
|
||||
space := $(null) #
|
||||
hyphen:= -
|
||||
|
|
|
@ -80,7 +80,14 @@ var (
|
|||
ErrMediaTypeNotSupported = errors.New("repodb: media type is not supported")
|
||||
ErrTimeout = errors.New("operation timeout")
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
ErrUnableToCreateUserBucket = errors.New("repodb: unable to create a user bucket for user")
|
||||
ErrDedupeRebuild = errors.New("dedupe: couldn't rebuild dedupe index")
|
||||
ErrMissingAuthHeader = errors.New("auth: required authorization header is missing")
|
||||
ErrUserAPIKeyNotFound = errors.New("userDB: user info for given API key hash not found")
|
||||
ErrUserSessionNotFound = errors.New("userDB: user session for given ID not found")
|
||||
ErrBucketDoesNotExist = errors.New("DB: bucket does not exist")
|
||||
ErrOpenIDProviderDoesNotExist = errors.New("openID: provider does not exist in given config")
|
||||
ErrHashKeyNotCreated = errors.New("cookiestore: generated random hash key is nil, not continuing")
|
||||
ErrFailedTypeAssertion = errors.New("type assertion failed")
|
||||
ErrInvalidOldUserStarredRepos = errors.New("repodb: invalid old entry for user starred repos")
|
||||
ErrUnmarshalledRepoListIsNil = errors.New("repodb: list of repos is still nil")
|
||||
ErrCouldNotMarshalStarredRepos = errors.New("repodb: could not repack entry for user starred repos")
|
||||
|
@ -89,7 +96,6 @@ var (
|
|||
ErrUserDataNotFound = errors.New("repodb: user data not found for given user identifier")
|
||||
ErrUserDataNotAllowed = errors.New("repodb: user data operations are not allowed")
|
||||
ErrCouldNotPersistData = errors.New("repodb: could not persist to db")
|
||||
ErrDedupeRebuild = errors.New("dedupe: couldn't rebuild dedupe index")
|
||||
ErrSignConfigDirNotSet = errors.New("signatures: signature config dir not set")
|
||||
ErrBadManifestDigest = errors.New("signatures: bad manifest digest")
|
||||
ErrInvalidSignatureType = errors.New("signatures: invalid signature type")
|
||||
|
@ -100,4 +106,5 @@ var (
|
|||
ErrInvalidTruststoreType = errors.New("signatures: invalid truststore type")
|
||||
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
|
||||
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content")
|
||||
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
|
||||
)
|
||||
|
|
|
@ -18,13 +18,20 @@ Examples of working configurations for various use cases are available [here](..
|
|||
|
||||
# Configuration Parameters
|
||||
|
||||
* [Network](#network)
|
||||
* [Storage](#storage)
|
||||
* [Authentication](#authentication)
|
||||
* [Identity-based Authorization](#identity-based-authorization)
|
||||
* [Logging](#logging)
|
||||
* [Metrics](#metrics)
|
||||
* [Sync](#sync)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Network](#network)
|
||||
- [Storage](#storage)
|
||||
- [Authentication](#authentication)
|
||||
- [TLS Mutual Authentication](#tls-mutual-authentication)
|
||||
- [Passphrase Authentication](#passphrase-authentication)
|
||||
- [Authentication Failures](#authentication-failures)
|
||||
- [API keys](#api-keys)
|
||||
- [Identity-based Authorization](#identity-based-authorization)
|
||||
- [Logging](#logging)
|
||||
- [Metrics](#metrics)
|
||||
- [Storage Drivers](#storage-drivers)
|
||||
- [Specifying S3 credentials](#specifying-s3-credentials)
|
||||
- [Sync](#sync)
|
||||
|
||||
|
||||
## Network
|
||||
|
@ -162,6 +169,98 @@ NOTE: When both htpasswd and LDAP configuration are specified, LDAP authenticati
|
|||
}
|
||||
```
|
||||
|
||||
### OpenID/OAuth2 social login
|
||||
|
||||
zot supports several openID/OAuth2 providers:
|
||||
- google
|
||||
- github
|
||||
- gitlab
|
||||
- dex
|
||||
|
||||
zot can be configured to use the above providers with:
|
||||
```
|
||||
{
|
||||
"http": {
|
||||
"auth": {
|
||||
"openid": {
|
||||
"providers": {
|
||||
"github": {
|
||||
"clientid": <client_id>,
|
||||
"clientsecret": <client_secret>,
|
||||
"scopes": ["read:org", "user", "repo"]
|
||||
},
|
||||
"google": {
|
||||
"issuer": "https://accounts.google.com",
|
||||
"clientid": <client_id>,
|
||||
"clientsecret": <client_secret>,
|
||||
"scopes": ["openid", "email"]
|
||||
},
|
||||
"gitlab": {
|
||||
"issuer": "https://gitlab.com",
|
||||
"clientid": <client_id>,
|
||||
"clientsecret": <client_secret>,
|
||||
"scopes": ["openid", "read_api", "read_user", "profile", "email"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
the login with either provider use http://127.0.0.1:8080/auth/login?provider=\<provider\>&callback_ui=http://127.0.0.1:8080/home
|
||||
for example to login with github use http://127.0.0.1:8080/auth/login?provider=github&callback_ui=http://127.0.0.1:8080/home
|
||||
|
||||
callback_ui query parameter is used by zot to redirect to UI after a successful openid/oauth2 authentication
|
||||
|
||||
the callback url which should be used when making oauth2 provider setup is http://127.0.0.1:8080/auth/callback/\<provider\>
|
||||
for example github callback url would be http://127.0.0.1:8080/auth/callback/github
|
||||
|
||||
If network policy doesn't allow inbound connections, this callback wont work!
|
||||
|
||||
dex is an identity service that uses OpenID Connect to drive authentication for other apps https://github.com/dexidp/dex
|
||||
To setup dex service see https://dexidp.io/docs/getting-started/
|
||||
|
||||
to configure zot as a client in dex (assuming zot is hosted at 127.0.0.1:8080), we need to configure dex with:
|
||||
|
||||
```
|
||||
staticClients:
|
||||
- id: zot-client
|
||||
redirectURIs:
|
||||
- 'http://127.0.0.1:8080/auth/callback/dex'
|
||||
name: 'zot'
|
||||
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
|
||||
```
|
||||
|
||||
zot can be configured to use dex with:
|
||||
|
||||
```
|
||||
"http": {
|
||||
"auth": {
|
||||
"openid": {
|
||||
"providers": {
|
||||
"dex": {
|
||||
"clientid": "zot-client",
|
||||
"clientsecret": "ZXhhbXBsZS1hcHAtc2VjcmV0",
|
||||
"keypath": "",
|
||||
"issuer": "http://127.0.0.1:5556/dex",
|
||||
"scopes": ["openid", "profile", "email", "groups"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
to login using openid dex provider use http://127.0.0.1:8080/auth/login?provider=dex
|
||||
|
||||
### Session based login
|
||||
|
||||
Whenever a user logs in zot using any of the auth options available(basic auth/openid) zot will set a 'session' cookie on its response.
|
||||
Using that cookie on subsequent calls will authenticate them, asumming the cookie didn't expire.
|
||||
|
||||
In case of using filesystem storage sessions are saved in zot's root directory.
|
||||
In case of using cloud storage sessions are saved in memory.
|
||||
|
||||
#### Authentication Failures
|
||||
|
||||
Should authentication fail, to prevent automated attacks, a delayed response can be configured with:
|
||||
|
@ -172,6 +271,21 @@ Should authentication fail, to prevent automated attacks, a delayed response can
|
|||
"failDelay": 5
|
||||
```
|
||||
|
||||
#### API keys
|
||||
|
||||
zot allows authentication for REST API calls using your API key as an alternative to your password.
|
||||
for more info see [API keys doc](../pkg/extensions/README_apikey.md)
|
||||
|
||||
To activate API keys use:
|
||||
|
||||
```
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Identity-based Authorization
|
||||
|
||||
Allowing actions on one or more repository paths can be tied to user
|
||||
|
|
121
examples/config-metricsbug.json
Normal file
121
examples/config-metricsbug.json
Normal file
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"distSpecVersion": "1.1.0-dev",
|
||||
"extensions": {
|
||||
"metrics": {
|
||||
"enable": true,
|
||||
"prometheus": {
|
||||
"path": "/metrics"
|
||||
}
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
},
|
||||
"scrub": {
|
||||
"enable": true,
|
||||
"interval": "24h"
|
||||
},
|
||||
"search": {
|
||||
"cve": {
|
||||
"updateInterval": "2h"
|
||||
},
|
||||
"enable": true
|
||||
},
|
||||
"sync": {
|
||||
"enable": true,
|
||||
"registries": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"destination": "/docker.io",
|
||||
"prefix": "**"
|
||||
}
|
||||
],
|
||||
"onDemand": true,
|
||||
"tlsVerify": true,
|
||||
"urls": [
|
||||
"https://docker.io/library"
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"destination": "/registry.gitlab.com",
|
||||
"prefix": "**"
|
||||
}
|
||||
],
|
||||
"onDemand": true,
|
||||
"tlsVerify": true,
|
||||
"urls": [
|
||||
"https://registry.gitlab.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"destination": "ghcr.io",
|
||||
"prefix": "**"
|
||||
}
|
||||
],
|
||||
"onDemand": true,
|
||||
"tlsVerify": true,
|
||||
"urls": [
|
||||
"https://ghcr.io"
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"destination": "/quay.io",
|
||||
"prefix": "**"
|
||||
}
|
||||
],
|
||||
"onDemand": true,
|
||||
"tlsVerify": true,
|
||||
"urls": [
|
||||
"https://quay.io"
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"destination": "/gcr.io",
|
||||
"prefix": "**"
|
||||
}
|
||||
],
|
||||
"onDemand": true,
|
||||
"tlsVerify": true,
|
||||
"urls": [
|
||||
"https://gcr.io"
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"destination": "/registry.k8s.io",
|
||||
"prefix": "**"
|
||||
}
|
||||
],
|
||||
"onDemand": true,
|
||||
"tlsVerify": true,
|
||||
"urls": [
|
||||
"https://registry.k8s.io"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ui": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"address": "0.0.0.0",
|
||||
"port": "5000"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"storage": {
|
||||
"gc": true,
|
||||
"rootDirectory": "/tmp/zot"
|
||||
}
|
||||
}
|
75
examples/config-openid.json
Normal file
75
examples/config-openid.json
Normal file
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"distSpecVersion": "1.1.0-dev",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot",
|
||||
"dedupe": true
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"auth": {
|
||||
"htpasswd": {
|
||||
"path": "test/data/htpasswd"
|
||||
},
|
||||
"openid": {
|
||||
"providers": {
|
||||
"github": {
|
||||
"clientid": "client_id",
|
||||
"clientsecret": "client_secret",
|
||||
"keypath": "",
|
||||
"scopes": ["read:org", "user", "repo"]
|
||||
},
|
||||
"google": {
|
||||
"issuer": "https://accounts.google.com",
|
||||
"clientid": "client_id",
|
||||
"clientsecret": "client_secret",
|
||||
"scopes": ["openid", "email"]
|
||||
},
|
||||
"gitlab": {
|
||||
"issuer": "https://gitlab.com",
|
||||
"clientid": "client_id",
|
||||
"clientsecret": "client_secret",
|
||||
"scopes": ["openid", "read_api", "read_user", "profile", "email"]
|
||||
},
|
||||
"dex": {
|
||||
"issuer": "http://127.0.0.1:5556/dex",
|
||||
"clientid": "client_id",
|
||||
"clientsecret": "client_secret",
|
||||
"scopes": ["openid", "user", "email", "groups"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"failDelay": 5
|
||||
},
|
||||
"accessControl": {
|
||||
"repositories": {
|
||||
"**": {
|
||||
"policies": [
|
||||
{
|
||||
"users": [
|
||||
"test"
|
||||
],
|
||||
"actions": [
|
||||
"read",
|
||||
"create"
|
||||
]
|
||||
}
|
||||
],
|
||||
"defaultPolicy": ["read"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": true
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
22
go.mod
22
go.mod
|
@ -20,7 +20,6 @@ require (
|
|||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/google/go-containerregistry v0.15.2
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.3
|
||||
github.com/json-iterator/go v1.1.12
|
||||
|
@ -53,14 +52,29 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.20.0
|
||||
github.com/containers/image/v5 v5.25.0
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/google/go-github/v52 v52.0.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/migueleliasweb/go-github-mock v0.0.18
|
||||
github.com/notaryproject/notation-go v1.0.0-rc.6
|
||||
github.com/opencontainers/distribution-spec/specs-go v0.0.0-20230117141039-067a0f5b0e25
|
||||
github.com/project-zot/mockoidc v0.0.0-20230307111146-f607b4b5fb97
|
||||
github.com/sigstore/cosign/v2 v2.0.2
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/zitadel/oidc v1.12.0
|
||||
golang.org/x/oauth2 v0.9.0
|
||||
modernc.org/sqlite v1.23.1
|
||||
oras.land/oras-go/v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-github/v50 v50.2.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 // indirect
|
||||
|
@ -108,7 +122,6 @@ require (
|
|||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/go-github/v50 v50.2.0 // indirect
|
||||
github.com/google/licenseclassifier/v2 v2.0.0 // indirect
|
||||
github.com/google/s2a-go v0.1.3 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
|
@ -331,6 +344,7 @@ require (
|
|||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/gorilla/schema v1.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
@ -452,11 +466,7 @@ require (
|
|||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/oauth2 v0.9.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/term v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
|
|
32
go.sum
32
go.sum
|
@ -72,6 +72,7 @@ cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQH
|
|||
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
|
||||
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
|
||||
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
|
||||
|
@ -287,6 +288,7 @@ github.com/Microsoft/hcsshim v0.10.0-rc.7/go.mod h1:ILuwjA+kNW+MrN/w5un7n3mTqkws
|
|||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
|
@ -501,6 +503,7 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oM
|
|||
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
|
||||
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
|
||||
|
@ -724,9 +727,11 @@ github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxF
|
|||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap/v3 v3.4.5 h1:ekEKmaDrpvR2yf5Nc/DClsGG9lAmdDixe44mLzlW5r8=
|
||||
github.com/go-ldap/ldap/v3 v3.4.5/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
|
@ -910,8 +915,12 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.14.1-0.20230409045903-ed5c185df419 h1:gMlTWagRJgCJ3EnISyF5+p9phYpFyWEI70Z56T+o2MY=
|
||||
github.com/google/go-containerregistry v0.14.1-0.20230409045903-ed5c185df419/go.mod h1:ETSJmRH9iO4Q0WQILIMkDUiKk+CaxItZW+gEDjyw8Ug=
|
||||
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
|
||||
github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUexMpIfk=
|
||||
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
|
||||
github.com/google/go-github/v52 v52.0.0 h1:uyGWOY+jMQ8GVGSX8dkSwCzlehU3WfdxQ7GweO/JP7M=
|
||||
github.com/google/go-github/v52 v52.0.0/go.mod h1:WJV6VEEUPuMo5pXqqa2ZCZEdbQqua4zAk2MZTIo+m+4=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
@ -984,6 +993,12 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
|
|||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
|
@ -1067,6 +1082,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
|
|||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU=
|
||||
github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU=
|
||||
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
|
||||
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
|
@ -1230,6 +1247,8 @@ github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
|||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/migueleliasweb/go-github-mock v0.0.18 h1:0lWt9MYmZQGnQE2rFtjlft/YtD6hzxuN6JJRFpujzEI=
|
||||
github.com/migueleliasweb/go-github-mock v0.0.18/go.mod h1:CcgXcbMoRnf3rRVHqGssuBquZDIcaplxL2W6G+xs7kM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
|
@ -1391,6 +1410,8 @@ github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8
|
|||
github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
|
||||
github.com/proglottis/gpgme v0.1.3 h1:Crxx0oz4LKB3QXc5Ea0J19K/3ICfy3ftr5exgUK1AU0=
|
||||
github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0=
|
||||
github.com/project-zot/mockoidc v0.0.0-20230307111146-f607b4b5fb97 h1:V6z9y0Yx2sQs4WSKx79mgkKJWwjbu/lHQg1yza5bmQE=
|
||||
github.com/project-zot/mockoidc v0.0.0-20230307111146-f607b4b5fb97/go.mod h1:46X30UrCsiwicZcg5L098Pyilaj94AO39mvS5PEyPn8=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
|
@ -1688,6 +1709,10 @@ github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd5
|
|||
github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
|
||||
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
||||
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM=
|
||||
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
||||
github.com/zitadel/oidc v1.12.0 h1:JNUjbhuuQRxFsfwd9s1CP3GOI4IwZ70G8sa3FQHPy6Y=
|
||||
github.com/zitadel/oidc v1.12.0/go.mod h1:uOTKHn4pqb0w7WbdGCMTxkmzZaBcyOmBKBNcptlF6f8=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
|
@ -1872,6 +1897,7 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -1900,6 +1926,7 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri
|
|||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
|
||||
golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -2007,6 +2034,7 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -2029,6 +2057,7 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
@ -2037,6 +2066,7 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -2052,6 +2082,7 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -2365,6 +2396,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
|
|
841
pkg/api/authn.go
841
pkg/api/authn.go
|
@ -3,139 +3,315 @@ package api
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chartmuseum/auth"
|
||||
"github.com/google/go-github/v52/github"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"github.com/zitadel/oidc/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/pkg/http"
|
||||
"github.com/zitadel/oidc/pkg/oidc"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/oauth2"
|
||||
githubOAuth "golang.org/x/oauth2/github"
|
||||
|
||||
"zotregistry.io/zot/errors"
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
apiErr "zotregistry.io/zot/pkg/api/errors"
|
||||
"zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageConstants "zotregistry.io/zot/pkg/storage/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
bearerAuthDefaultAccessEntryType = "repository"
|
||||
issuedAtOffset = 5 * time.Second
|
||||
relyingPartyCookieMaxAge = 120
|
||||
)
|
||||
|
||||
func AuthHandler(c *Controller) mux.MiddlewareFunc {
|
||||
if isBearerAuthEnabled(c.Config) {
|
||||
return bearerAuthHandler(c)
|
||||
}
|
||||
|
||||
return basicAuthHandler(c)
|
||||
type AuthnMiddleware struct {
|
||||
credMap map[string]string
|
||||
ldapClient *LDAPClient
|
||||
}
|
||||
|
||||
func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{
|
||||
Realm: ctlr.Config.HTTP.Auth.Bearer.Realm,
|
||||
Service: ctlr.Config.HTTP.Auth.Bearer.Service,
|
||||
PublicKeyPath: ctlr.Config.HTTP.Auth.Bearer.Cert,
|
||||
AccessEntryType: bearerAuthDefaultAccessEntryType,
|
||||
EmptyDefaultNamespace: true,
|
||||
})
|
||||
func AuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
authnMiddleware := &AuthnMiddleware{}
|
||||
|
||||
if isBearerAuthEnabled(ctlr.Config) {
|
||||
return bearerAuthHandler(ctlr)
|
||||
}
|
||||
|
||||
return authnMiddleware.TryAuthnHandlers(ctlr)
|
||||
}
|
||||
|
||||
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, next http.Handler, response http.ResponseWriter,
|
||||
request *http.Request, delay int,
|
||||
) {
|
||||
clientHeader := request.Header.Get(constants.SessionClientHeaderName)
|
||||
if clientHeader != constants.SessionClientHeaderValue {
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
identity, ok := common.GetAuthUserFromRequestSession(ctlr.CookieStore, request, ctlr.Log)
|
||||
if !ok {
|
||||
// let the client know that this session is invalid/expired
|
||||
cookie := &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
http.SetCookie(response, cookie)
|
||||
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, []string{}, request)
|
||||
|
||||
groups, err := ctlr.RepoDB.GetUserGroups(ctx)
|
||||
if err != nil {
|
||||
ctlr.Log.Panic().Err(err).Msg("error creating bearer authorizer")
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not find user profile in DB")
|
||||
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not get user profile in DB")
|
||||
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
next.ServeHTTP(response, request)
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
ctx = getReqContextWithAuthorization(identity, groups, request)
|
||||
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(request)
|
||||
name := vars["name"]
|
||||
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
header := request.Header.Get("Authorization")
|
||||
|
||||
if (header == "" || header == "Basic Og==") && isMgmtRequested {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
action := auth.PullAction
|
||||
if m := request.Method; m != http.MethodGet && m != http.MethodHead {
|
||||
action = auth.PushAction
|
||||
}
|
||||
permissions, err := authorizer.Authorize(header, action, name)
|
||||
if err != nil {
|
||||
ctlr.Log.Error().Err(err).Msg("issue parsing Authorization header")
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
common.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !permissions.Allowed {
|
||||
authFail(response, permissions.WWWAuthenticateHeader, 0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
})
|
||||
}
|
||||
next.ServeHTTP(response, request.WithContext(ctx))
|
||||
}
|
||||
|
||||
func noPasswdAuth(realm string, config *config.Config) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
next.ServeHTTP(response, request)
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseWriter,
|
||||
request *http.Request,
|
||||
) (bool, http.ResponseWriter, *http.Request, error) {
|
||||
cookieStore := ctlr.CookieStore
|
||||
|
||||
return
|
||||
}
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
// Process request
|
||||
if request.Header.Get("Authorization") == "" {
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
})
|
||||
// Process request
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
identity, passphrase, err := getUsernamePasswordBasicAuth(request)
|
||||
if err != nil {
|
||||
ctlr.Log.Error().Err(err).Msg("failed to parse authorization header")
|
||||
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
// some client tools might send Authorization: Basic Og== (decoded into ":")
|
||||
// empty username and password
|
||||
if identity == "" && passphrase == "" {
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
passphraseHash, ok := amw.credMap[identity]
|
||||
if ok {
|
||||
// first, HTTPPassword authN (which is local)
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err == nil {
|
||||
// Process request
|
||||
var groups []string
|
||||
|
||||
if ctlr.Config.HTTP.AccessControl != nil {
|
||||
ac := NewAccessController(ctlr.Config)
|
||||
groups = ac.getUserGroups(identity)
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, groups, request)
|
||||
|
||||
// saved logged session
|
||||
if err := saveUserLoggedSession(cookieStore, response, request, identity, ctlr.Log); err != nil {
|
||||
return false, response, request, err
|
||||
}
|
||||
|
||||
if err := ctlr.RepoDB.SetUserGroups(ctx, groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", identity).Msg("couldn't update user profile")
|
||||
|
||||
return false, response, request, err
|
||||
}
|
||||
|
||||
ctlr.Log.Info().Str("identity", identity).Msgf("user profile successfully set")
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
// next, LDAP if configured (network-based which can lose connectivity)
|
||||
if ctlr.Config.HTTP.Auth != nil && ctlr.Config.HTTP.Auth.LDAP != nil {
|
||||
ok, _, ldapgroups, err := amw.ldapClient.Authenticate(identity, passphrase)
|
||||
if ok && err == nil {
|
||||
// Process request
|
||||
var groups []string
|
||||
|
||||
if ctlr.Config.HTTP.AccessControl != nil {
|
||||
ac := NewAccessController(ctlr.Config)
|
||||
groups = ac.getUserGroups(identity)
|
||||
}
|
||||
|
||||
groups = append(groups, ldapgroups...)
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, groups, request)
|
||||
|
||||
if err := saveUserLoggedSession(cookieStore, response, request, identity, ctlr.Log); err != nil {
|
||||
return false, response, request, err
|
||||
}
|
||||
|
||||
if err := ctlr.RepoDB.SetUserGroups(ctx, groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", identity).Msg("couldn't update user profile")
|
||||
|
||||
return false, response, request, err
|
||||
}
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
// last try API keys
|
||||
if isAPIKeyEnabled(ctlr.Config) {
|
||||
apiKey := passphrase
|
||||
|
||||
if !strings.HasPrefix(apiKey, constants.APIKeysPrefix) {
|
||||
ctlr.Log.Error().Msg("api token has invalid format")
|
||||
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
trimmedAPIKey := strings.TrimPrefix(apiKey, constants.APIKeysPrefix)
|
||||
|
||||
hashedKey := hashUUID(trimmedAPIKey)
|
||||
|
||||
storedIdentity, err := ctlr.RepoDB.GetUserAPIKeyInfo(hashedKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrUserAPIKeyNotFound) {
|
||||
ctlr.Log.Info().Err(err).Msgf("can not find any user info for hashed key %s in DB", hashedKey)
|
||||
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
ctlr.Log.Error().Err(err).Msgf("can not get user info for hashed key %s in DB", hashedKey)
|
||||
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
if storedIdentity == identity {
|
||||
ctx := getReqContextWithAuthorization(identity, []string{}, request)
|
||||
|
||||
err := ctlr.RepoDB.UpdateUserAPIKeyLastUsed(ctx, hashedKey)
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not update user profile in DB")
|
||||
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
groups, err := ctlr.RepoDB.GetUserGroups(ctx)
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not get user's groups in DB")
|
||||
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
ctx = getReqContextWithAuthorization(identity, groups, request)
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo // we use closure making this a complex subroutine
|
||||
func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
realm := ctlr.Config.HTTP.Realm
|
||||
if realm == "" {
|
||||
realm = "Authorization Required"
|
||||
}
|
||||
|
||||
realm = "Basic realm=" + strconv.Quote(realm)
|
||||
|
||||
func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFunc { //nolint: gocyclo
|
||||
// no password based authN, if neither LDAP nor HTTP BASIC is enabled
|
||||
if ctlr.Config.HTTP.Auth == nil ||
|
||||
(ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil) {
|
||||
return noPasswdAuth(realm, ctlr.Config)
|
||||
(ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil &&
|
||||
ctlr.Config.HTTP.Auth.OpenID == nil) {
|
||||
return noPasswdAuth(ctlr.Config)
|
||||
}
|
||||
|
||||
credMap := make(map[string]string)
|
||||
amw.credMap = make(map[string]string)
|
||||
|
||||
delay := ctlr.Config.HTTP.Auth.FailDelay
|
||||
|
||||
var ldapClient *LDAPClient
|
||||
// setup sessions cookie store used to preserve logged in user in web sessions
|
||||
if isAuthnEnabled(ctlr.Config) || isOpenIDAuthEnabled(ctlr.Config) {
|
||||
// To store custom types in our cookies,
|
||||
// we must first register them using gob.Register
|
||||
gob.Register(map[string]interface{}{})
|
||||
|
||||
cookieStoreHashKey := securecookie.GenerateRandomKey(64)
|
||||
if cookieStoreHashKey == nil {
|
||||
panic(zerr.ErrHashKeyNotCreated)
|
||||
}
|
||||
|
||||
// if storage is filesystem then use zot's rootDir to store sessions
|
||||
if ctlr.Config.Storage.StorageDriver == nil {
|
||||
sessionsDir := path.Join(ctlr.Config.Storage.RootDirectory, "_sessions")
|
||||
if err := os.MkdirAll(sessionsDir, storageConstants.DefaultDirPerms); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cookieStore := sessions.NewFilesystemStore(sessionsDir, cookieStoreHashKey)
|
||||
|
||||
cookieStore.MaxAge(cookiesMaxAge)
|
||||
|
||||
ctlr.CookieStore = cookieStore
|
||||
} else {
|
||||
cookieStore := sessions.NewCookieStore(cookieStoreHashKey)
|
||||
|
||||
cookieStore.MaxAge(cookiesMaxAge)
|
||||
|
||||
ctlr.CookieStore = cookieStore
|
||||
}
|
||||
}
|
||||
|
||||
// ldap and htpasswd based authN
|
||||
if ctlr.Config.HTTP.Auth != nil {
|
||||
if ctlr.Config.HTTP.Auth.LDAP != nil {
|
||||
ldapConfig := ctlr.Config.HTTP.Auth.LDAP
|
||||
ldapClient = &LDAPClient{
|
||||
amw.ldapClient = &LDAPClient{
|
||||
Host: ldapConfig.Address,
|
||||
Port: ldapConfig.Port,
|
||||
UseSSL: !ldapConfig.Insecure,
|
||||
|
@ -160,18 +336,18 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
caCertPool := x509.NewCertPool()
|
||||
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
panic(errors.ErrBadCACert)
|
||||
panic(zerr.ErrBadCACert)
|
||||
}
|
||||
|
||||
ldapClient.ClientCAs = caCertPool
|
||||
amw.ldapClient.ClientCAs = caCertPool
|
||||
} else {
|
||||
// default to system cert pool
|
||||
caCertPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
panic(errors.ErrBadCACert)
|
||||
panic(zerr.ErrBadCACert)
|
||||
}
|
||||
|
||||
ldapClient.ClientCAs = caCertPool
|
||||
amw.ldapClient.ClientCAs = caCertPool
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,12 +364,27 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
line := scanner.Text()
|
||||
if strings.Contains(line, ":") {
|
||||
tokens := strings.Split(scanner.Text(), ":")
|
||||
credMap[tokens[0]] = tokens[1]
|
||||
amw.credMap[tokens[0]] = tokens[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// openid based authN
|
||||
if ctlr.Config.HTTP.Auth.OpenID != nil {
|
||||
ctlr.RelyingParties = make(map[string]rp.RelyingParty)
|
||||
|
||||
for provider := range ctlr.Config.HTTP.Auth.OpenID.Providers {
|
||||
if IsOpenIDSupported(provider) {
|
||||
rp := NewRelyingPartyOIDC(ctlr.Config, provider)
|
||||
ctlr.RelyingParties[provider] = rp
|
||||
} else if IsOauth2Supported(provider) {
|
||||
rp := NewRelyingPartyGithub(ctlr.Config, provider)
|
||||
ctlr.RelyingParties[provider] = rp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
|
@ -203,84 +394,231 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
if request.Header.Get("Authorization") == "" {
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
// Process request
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
username, passphrase, err := getUsernamePasswordBasicAuth(request)
|
||||
//nolint: contextcheck
|
||||
authenticated, cloneResp, cloneReq, err := amw.basicAuthn(ctlr, response, request)
|
||||
if err != nil {
|
||||
ctlr.Log.Error().Err(err).Msg("failed to parse authorization header")
|
||||
authFail(response, realm, delay)
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// some client tools might send Authorization: Basic Og== (decoded into ":")
|
||||
// empty username and password
|
||||
if username == "" && passphrase == "" {
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
// Process request
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
if authenticated && cloneResp != nil && cloneReq != nil {
|
||||
next.ServeHTTP(cloneResp, cloneReq)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// first, HTTPPassword authN (which is local)
|
||||
passphraseHash, ok := credMap[username]
|
||||
if ok {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err == nil {
|
||||
// Process request
|
||||
var userGroups []string
|
||||
|
||||
if ctlr.Config.HTTP.AccessControl != nil {
|
||||
ac := NewAccessController(ctlr.Config)
|
||||
userGroups = ac.getUserGroups(username)
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(username, userGroups, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// next, LDAP if configured (network-based which can lose connectivity)
|
||||
if ctlr.Config.HTTP.Auth != nil && ctlr.Config.HTTP.Auth.LDAP != nil {
|
||||
ok, _, ldapgroups, err := ldapClient.Authenticate(username, passphrase)
|
||||
if ok && err == nil {
|
||||
// Process request
|
||||
var userGroups []string
|
||||
|
||||
if ctlr.Config.HTTP.AccessControl != nil {
|
||||
ac := NewAccessController(ctlr.Config)
|
||||
userGroups = ac.getUserGroups(username)
|
||||
}
|
||||
|
||||
userGroups = append(userGroups, ldapgroups...)
|
||||
|
||||
ctx := getReqContextWithAuthorization(username, userGroups, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
authFail(response, realm, delay)
|
||||
//nolint: contextcheck
|
||||
amw.sessionAuthn(ctlr, next, response, request, delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{
|
||||
Realm: ctlr.Config.HTTP.Auth.Bearer.Realm,
|
||||
Service: ctlr.Config.HTTP.Auth.Bearer.Service,
|
||||
PublicKeyPath: ctlr.Config.HTTP.Auth.Bearer.Cert,
|
||||
AccessEntryType: bearerAuthDefaultAccessEntryType,
|
||||
EmptyDefaultNamespace: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctlr.Log.Panic().Err(err).Msg("error creating bearer authorizer")
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
next.ServeHTTP(response, request)
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
|
||||
return
|
||||
}
|
||||
acCtrlr := NewAccessController(ctlr.Config)
|
||||
vars := mux.Vars(request)
|
||||
name := vars["name"]
|
||||
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
header := request.Header.Get("Authorization")
|
||||
|
||||
if (header == "" || header == "Basic Og==") && isMgmtRequested {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
action := auth.PullAction
|
||||
if m := request.Method; m != http.MethodGet && m != http.MethodHead {
|
||||
action = auth.PushAction
|
||||
}
|
||||
|
||||
permissions, err := authorizer.Authorize(header, action, name)
|
||||
if err != nil {
|
||||
ctlr.Log.Error().Err(err).Msg("issue parsing Authorization header")
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
common.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !permissions.Allowed {
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.Header().Set("WWW-Authenticate", permissions.WWWAuthenticateHeader)
|
||||
|
||||
common.WriteJSON(response, http.StatusUnauthorized,
|
||||
apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
amCtx := acCtrlr.getAuthnMiddlewareContext(BEARER, request)
|
||||
next.ServeHTTP(response, request.WithContext(amCtx)) //nolint:contextcheck
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func noPasswdAuth(config *config.Config) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
next.ServeHTTP(response, request)
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
// Process request
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *RouteHandler) AuthURLHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
callbackUI := query.Get(constants.CallbackUIQueryParam)
|
||||
|
||||
provider := query.Get("provider")
|
||||
|
||||
client, ok := rh.c.RelyingParties[provider]
|
||||
if !ok {
|
||||
http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
})(w, r)
|
||||
}
|
||||
|
||||
/* save cookie containing state to later verify it and
|
||||
callback ui where we will redirect after openid/oauth2 logic is completed*/
|
||||
session, _ := rh.c.CookieStore.Get(r, "statecookie")
|
||||
|
||||
session.Options.Secure = true
|
||||
session.Options.HttpOnly = true
|
||||
session.Options.SameSite = http.SameSiteDefaultMode
|
||||
session.Options.Path = constants.CallbackBasePath
|
||||
|
||||
state := uuid.New().String()
|
||||
|
||||
session.Values["state"] = state
|
||||
session.Values["callback"] = callbackUI
|
||||
|
||||
// let the session set its own id
|
||||
err := session.Save(r, w)
|
||||
if err != nil {
|
||||
rh.c.Log.Error().Err(err).Msg("unable to save http session")
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
stateFunc := func() string {
|
||||
return state
|
||||
}
|
||||
|
||||
rp.AuthURLHandler(stateFunc, client)(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func NewRelyingPartyOIDC(config *config.Config, provider string) rp.RelyingParty {
|
||||
issuer, clientID, clientSecret, redirectURI, scopes, options := getRelyingPartyArgs(config, provider)
|
||||
|
||||
relyingParty, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return relyingParty
|
||||
}
|
||||
|
||||
func NewRelyingPartyGithub(config *config.Config, provider string) rp.RelyingParty {
|
||||
_, clientID, clientSecret, redirectURI, scopes, options := getRelyingPartyArgs(config, provider)
|
||||
|
||||
rpConfig := &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectURI,
|
||||
Scopes: scopes,
|
||||
Endpoint: githubOAuth.Endpoint,
|
||||
}
|
||||
|
||||
relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, options...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return relyingParty
|
||||
}
|
||||
|
||||
func getRelyingPartyArgs(config *config.Config, provider string) (
|
||||
string, string, string, string, []string, []rp.Option,
|
||||
) {
|
||||
if _, ok := config.HTTP.Auth.OpenID.Providers[provider]; !ok {
|
||||
panic(zerr.ErrOpenIDProviderDoesNotExist)
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if config.HTTP.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
clientID := config.HTTP.Auth.OpenID.Providers[provider].ClientID
|
||||
clientSecret := config.HTTP.Auth.OpenID.Providers[provider].ClientSecret
|
||||
|
||||
scopes := config.HTTP.Auth.OpenID.Providers[provider].Scopes
|
||||
// openid scope must be the first one in list
|
||||
if !common.Contains(scopes, oidc.ScopeOpenID) && IsOpenIDSupported(provider) {
|
||||
scopes = append([]string{oidc.ScopeOpenID}, scopes...)
|
||||
}
|
||||
|
||||
port := config.HTTP.Port
|
||||
issuer := config.HTTP.Auth.OpenID.Providers[provider].Issuer
|
||||
keyPath := config.HTTP.Auth.OpenID.Providers[provider].KeyPath
|
||||
baseURL := net.JoinHostPort(config.HTTP.Address, port)
|
||||
redirectURI := fmt.Sprintf("%s://%s%s", scheme, baseURL, constants.CallbackBasePath+fmt.Sprintf("/%s", provider))
|
||||
|
||||
options := []rp.Option{
|
||||
rp.WithVerifierOpts(rp.WithIssuedAtOffset(issuedAtOffset)),
|
||||
}
|
||||
|
||||
key := securecookie.GenerateRandomKey(32) //nolint: gomnd
|
||||
|
||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithMaxAge(relyingPartyCookieMaxAge))
|
||||
options = append(options, rp.WithCookieHandler(cookieHandler))
|
||||
|
||||
if clientSecret == "" {
|
||||
options = append(options, rp.WithPKCE(cookieHandler))
|
||||
}
|
||||
|
||||
if keyPath != "" {
|
||||
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
||||
}
|
||||
|
||||
return issuer, clientID, clientSecret, redirectURI, scopes, options
|
||||
}
|
||||
|
||||
func getReqContextWithAuthorization(username string, groups []string, request *http.Request) context.Context {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: username,
|
||||
|
@ -314,9 +652,71 @@ func isBearerAuthEnabled(config *config.Config) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func authFail(w http.ResponseWriter, realm string, delay int) {
|
||||
func isOpenIDAuthEnabled(config *config.Config) bool {
|
||||
if config.HTTP.Auth != nil &&
|
||||
config.HTTP.Auth.OpenID != nil {
|
||||
for provider := range config.HTTP.Auth.OpenID.Providers {
|
||||
if isOpenIDAuthProviderEnabled(config, provider) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isAPIKeyEnabled(config *config.Config) bool {
|
||||
if config.Extensions != nil && config.Extensions.APIKey != nil &&
|
||||
*config.Extensions.APIKey.Enable {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isOpenIDAuthProviderEnabled(config *config.Config, provider string) bool {
|
||||
if providerConfig, ok := config.HTTP.Auth.OpenID.Providers[provider]; ok {
|
||||
if IsOpenIDSupported(provider) {
|
||||
if providerConfig.ClientID != "" || providerConfig.Issuer != "" ||
|
||||
len(providerConfig.Scopes) > 0 {
|
||||
return true
|
||||
}
|
||||
} else if IsOauth2Supported(provider) {
|
||||
if providerConfig.ClientID != "" || len(providerConfig.Scopes) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOpenIDSupported(provider string) bool {
|
||||
supported := []string{"google", "gitlab", "dex"}
|
||||
|
||||
return common.Contains(supported, provider)
|
||||
}
|
||||
|
||||
func IsOauth2Supported(provider string) bool {
|
||||
supported := []string{"github"}
|
||||
|
||||
return common.Contains(supported, provider)
|
||||
}
|
||||
|
||||
func authFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
w.Header().Set("WWW-Authenticate", realm)
|
||||
|
||||
// don't send auth headers if request is coming from UI
|
||||
if r.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
|
||||
if realm == "" {
|
||||
realm = "Authorization Required"
|
||||
}
|
||||
|
||||
realm = "Basic realm=" + strconv.Quote(realm)
|
||||
|
||||
w.Header().Set("WWW-Authenticate", realm)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
common.WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
|
||||
}
|
||||
|
@ -325,12 +725,12 @@ func getUsernamePasswordBasicAuth(request *http.Request) (string, string, error)
|
|||
basicAuth := request.Header.Get("Authorization")
|
||||
|
||||
if basicAuth == "" {
|
||||
return "", "", errors.ErrParsingAuthHeader
|
||||
return "", "", zerr.ErrParsingAuthHeader
|
||||
}
|
||||
|
||||
splitStr := strings.SplitN(basicAuth, " ", 2) //nolint:gomnd
|
||||
splitStr := strings.SplitN(basicAuth, " ", 2) //nolint: gomnd
|
||||
if len(splitStr) != 2 || strings.ToLower(splitStr[0]) != "basic" {
|
||||
return "", "", errors.ErrParsingAuthHeader
|
||||
return "", "", zerr.ErrParsingAuthHeader
|
||||
}
|
||||
|
||||
decodedStr, err := base64.StdEncoding.DecodeString(splitStr[1])
|
||||
|
@ -338,9 +738,9 @@ func getUsernamePasswordBasicAuth(request *http.Request) (string, string, error)
|
|||
return "", "", err
|
||||
}
|
||||
|
||||
pair := strings.SplitN(string(decodedStr), ":", 2) //nolint:gomnd
|
||||
if len(pair) != 2 { //nolint:gomnd
|
||||
return "", "", errors.ErrParsingAuthHeader
|
||||
pair := strings.SplitN(string(decodedStr), ":", 2) //nolint: gomnd
|
||||
if len(pair) != 2 { //nolint: gomnd
|
||||
return "", "", zerr.ErrParsingAuthHeader
|
||||
}
|
||||
|
||||
username := pair[0]
|
||||
|
@ -348,3 +748,118 @@ func getUsernamePasswordBasicAuth(request *http.Request) (string, string, error)
|
|||
|
||||
return username, passphrase, nil
|
||||
}
|
||||
|
||||
func GetGithubUserInfo(ctx context.Context, client *github.Client, log log.Logger) (string, []string, error) {
|
||||
var primaryEmail string
|
||||
|
||||
userEmails, _, err := client.Users.ListEmails(ctx, nil)
|
||||
if err != nil {
|
||||
log.Error().Msg("couldn't set user record for empty email value")
|
||||
|
||||
return "", []string{}, err
|
||||
}
|
||||
|
||||
if len(userEmails) != 0 {
|
||||
for _, email := range userEmails { // should have at least one primary email, if any
|
||||
if email.GetPrimary() { // check if it's primary email
|
||||
primaryEmail = email.GetEmail()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orgs, _, err := client.Organizations.List(ctx, "", nil)
|
||||
if err != nil {
|
||||
log.Error().Msg("couldn't set user record for empty email value")
|
||||
|
||||
return "", []string{}, err
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
for _, org := range orgs {
|
||||
groups = append(groups, *org.Login)
|
||||
}
|
||||
|
||||
return primaryEmail, groups, nil
|
||||
}
|
||||
|
||||
func saveUserLoggedSession(cookieStore sessions.Store, response http.ResponseWriter,
|
||||
request *http.Request, identity string, log log.Logger,
|
||||
) error {
|
||||
session, _ := cookieStore.Get(request, "session")
|
||||
|
||||
session.Options.Secure = true
|
||||
session.Options.HttpOnly = true
|
||||
session.Options.SameSite = http.SameSiteDefaultMode
|
||||
session.Values["authStatus"] = true
|
||||
session.Values["user"] = identity
|
||||
|
||||
// let the session set its own id
|
||||
err := session.Save(request, response)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("identity", identity).Msg("unable to save http session")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
userInfoCookie := sessions.NewCookie("user", identity, &sessions.Options{
|
||||
Secure: true,
|
||||
HttpOnly: false,
|
||||
MaxAge: cookiesMaxAge,
|
||||
SameSite: http.SameSiteDefaultMode,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
http.SetCookie(response, userInfoCookie)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OAuth2Callback is the callback logic where openid/oauth2 will redirect back to our app.
|
||||
func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, state, email string,
|
||||
groups []string,
|
||||
) (string, error) {
|
||||
stateCookie, _ := ctlr.CookieStore.Get(r, "statecookie")
|
||||
|
||||
stateOrigin, ok := stateCookie.Values["state"].(string)
|
||||
if !ok {
|
||||
ctlr.Log.Error().Err(zerr.ErrInvalidStateCookie).Msg("openID: unable to get 'state' cookie from request")
|
||||
|
||||
return "", zerr.ErrInvalidStateCookie
|
||||
}
|
||||
|
||||
if stateOrigin != state {
|
||||
ctlr.Log.Error().Err(zerr.ErrInvalidStateCookie).Msg("openID: 'state' cookie differs from the actual one")
|
||||
|
||||
return "", zerr.ErrInvalidStateCookie
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(email, groups, r)
|
||||
|
||||
// if this line has been reached, then a new session should be created
|
||||
// if the `session` key is already on the cookie, it's not a valid one
|
||||
if err := saveUserLoggedSession(ctlr.CookieStore, w, r, email, ctlr.Log); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := ctlr.RepoDB.SetUserGroups(ctx, groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", email).Msg("couldn't update the user profile")
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctlr.Log.Info().Msgf("user profile set successfully for email %s", email)
|
||||
|
||||
// redirect to UI
|
||||
callbackUI, _ := stateCookie.Values["callback"].(string)
|
||||
|
||||
return callbackUI, nil
|
||||
}
|
||||
|
||||
func hashUUID(uuid string) string {
|
||||
digester := sha256.New()
|
||||
digester.Write([]byte(uuid))
|
||||
|
||||
return godigest.NewDigestFromEncoded(godigest.SHA256, fmt.Sprintf("%x", digester.Sum(nil))).Encoded()
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ const (
|
|||
Delete = "delete"
|
||||
// behaviour actions.
|
||||
DetectManifestCollision = "detectManifestCollision"
|
||||
BASIC = "Basic"
|
||||
BEARER = "Bearer"
|
||||
OPENID = "OpenID"
|
||||
)
|
||||
|
||||
// AccessController authorizes users to act on resources.
|
||||
|
@ -29,10 +32,17 @@ type AccessController struct {
|
|||
Log log.Logger
|
||||
}
|
||||
|
||||
func NewAccessController(config *config.Config) *AccessController {
|
||||
func NewAccessController(conf *config.Config) *AccessController {
|
||||
if conf.HTTP.AccessControl == nil {
|
||||
return &AccessController{
|
||||
Config: &config.AccessControlConfig{},
|
||||
Log: log.NewLogger(conf.Log.Level, conf.Log.Output),
|
||||
}
|
||||
}
|
||||
|
||||
return &AccessController{
|
||||
Config: config.HTTP.AccessControl,
|
||||
Log: log.NewLogger(config.Log.Level, config.Log.Output),
|
||||
Config: conf.HTTP.AccessControl,
|
||||
Log: log.NewLogger(conf.Log.Level, conf.Log.Output),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,6 +181,18 @@ func (ac *AccessController) getContext(acCtx *localCtx.AccessControlContext, req
|
|||
return ctx
|
||||
}
|
||||
|
||||
// getAuthnMiddlewareContext builds ac context(allowed to read repos and if user is admin) and returns it.
|
||||
func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request *http.Request) context.Context {
|
||||
amwCtx := localCtx.AuthnMiddlewareContext{
|
||||
AuthnType: authnType,
|
||||
}
|
||||
|
||||
amwCtxKey := localCtx.GetAuthnMiddlewareCtxKey()
|
||||
ctx := context.WithValue(request.Context(), amwCtxKey, amwCtx)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// isPermitted returns true if username can do action on a repository policy.
|
||||
func (ac *AccessController) isPermitted(userGroups []string, username, action string,
|
||||
policyGroup config.PolicyGroup,
|
||||
|
@ -231,6 +253,14 @@ func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// request comes from bearer authn, bypass it
|
||||
authnMwCtx, err := localCtx.GetAuthnMiddlewareContext(request.Context())
|
||||
if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// bypass authz for /v2/ route
|
||||
if request.RequestURI == "/v2/" {
|
||||
next.ServeHTTP(response, request)
|
||||
|
@ -242,8 +272,6 @@ func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
var identity string
|
||||
|
||||
var err error
|
||||
|
||||
// anonymous context
|
||||
acCtx := &localCtx.AccessControlContext{}
|
||||
|
||||
|
@ -252,7 +280,7 @@ func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
// get access control context made in authn.go if authn is enabled
|
||||
acCtx, err = localCtx.GetAccessControlContext(request.Context())
|
||||
if err != nil { // should never happen
|
||||
authFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -272,7 +300,7 @@ func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
// if we still don't have an identity
|
||||
if identity == "" {
|
||||
acCtrlr.Log.Info().Msg("couldn't get identity from TLS certificate")
|
||||
authFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -298,6 +326,14 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// request comes from bearer authn, bypass it
|
||||
authnMwCtx, err := localCtx.GetAuthnMiddlewareContext(request.Context())
|
||||
if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(request)
|
||||
resource := vars["name"]
|
||||
reference, ok := vars["reference"]
|
||||
|
@ -306,12 +342,10 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
var identity string
|
||||
|
||||
var err error
|
||||
|
||||
// get acCtx built in authn and previous authz middlewares
|
||||
acCtx, err := localCtx.GetAccessControlContext(request.Context())
|
||||
if err != nil { // should never happen
|
||||
authFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -344,7 +378,7 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
can := acCtrlr.can(request.Context(), identity, action, resource) //nolint:contextcheck
|
||||
if !can {
|
||||
common.AuthzFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
common.AuthzFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
} else {
|
||||
next.ServeHTTP(response, request) //nolint:contextcheck
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ type AuthConfig struct {
|
|||
HTPasswd AuthHTPasswd
|
||||
LDAP *LDAPConfig
|
||||
Bearer *BearerConfig
|
||||
OpenID *OpenIDConfig
|
||||
}
|
||||
|
||||
type BearerConfig struct {
|
||||
|
@ -53,6 +54,18 @@ type BearerConfig struct {
|
|||
Cert string
|
||||
}
|
||||
|
||||
type OpenIDConfig struct {
|
||||
Providers map[string]OpenIDProviderConfig
|
||||
}
|
||||
|
||||
type OpenIDProviderConfig struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
KeyPath string
|
||||
Issuer string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
type MethodRatelimitConfig struct {
|
||||
Method string
|
||||
Rate int
|
||||
|
@ -63,6 +76,7 @@ type RatelimitConfig struct {
|
|||
Methods []MethodRatelimitConfig `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
//nolint:maligned
|
||||
type HTTPConfig struct {
|
||||
Address string
|
||||
Port string
|
||||
|
|
|
@ -12,4 +12,11 @@ const (
|
|||
DefaultMediaType = "application/json"
|
||||
BinaryMediaType = "application/octet-stream"
|
||||
DefaultMetricsExtensionRoute = "/metrics"
|
||||
CallbackBasePath = "/auth/callback"
|
||||
LoginPath = "/auth/login"
|
||||
LogoutPath = "/auth/logout"
|
||||
SessionClientHeaderName = "X-ZOT-API-CLIENT"
|
||||
SessionClientHeaderValue = "zot-ui"
|
||||
APIKeysPrefix = "zak_"
|
||||
CallbackUIQueryParam = "callback_ui"
|
||||
)
|
||||
|
|
|
@ -18,4 +18,7 @@ const (
|
|||
ExtUserPreferences = "/userprefs"
|
||||
ExtUserPreferencesPrefix = ExtPrefix + ExtUserPreferences
|
||||
FullUserPreferencesPrefix = RoutePrefix + ExtUserPreferencesPrefix
|
||||
ExtAPIKey = "/apikey"
|
||||
ExtAPIKeyPrefix = ExtPrefix + ExtAPIKey //nolint: gosec
|
||||
FullAPIKeyPrefix = RoutePrefix + ExtAPIKeyPrefix
|
||||
)
|
||||
|
|
|
@ -16,6 +16,8 @@ import (
|
|||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/zitadel/oidc/pkg/client/rp"
|
||||
|
||||
"zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
|
@ -31,6 +33,7 @@ import (
|
|||
const (
|
||||
idleTimeout = 120 * time.Second
|
||||
readHeaderTimeout = 5 * time.Second
|
||||
cookiesMaxAge = 86400 // seconds
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
|
@ -44,6 +47,8 @@ type Controller struct {
|
|||
Metrics monitoring.MetricServer
|
||||
CveInfo ext.CveInfo
|
||||
SyncOnDemand SyncOnDemand
|
||||
RelyingParties map[string]rp.RelyingParty
|
||||
CookieStore sessions.Store
|
||||
// runtime params
|
||||
chosenPort int // kernel-chosen port
|
||||
}
|
||||
|
@ -254,7 +259,9 @@ func (c *Controller) InitImageStore() error {
|
|||
}
|
||||
|
||||
func (c *Controller) InitRepoDB(reloadCtx context.Context) error {
|
||||
if c.Config.Extensions != nil && c.Config.Extensions.Search != nil && *c.Config.Extensions.Search.Enable {
|
||||
// init repoDB if search is enabled or authn enabled (need to store user profiles) or apikey ext is enabled
|
||||
if (c.Config.Extensions != nil && c.Config.Extensions.Search != nil && *c.Config.Extensions.Search.Enable) ||
|
||||
isAuthnEnabled(c.Config) || isOpenIDAuthEnabled(c.Config) || isAPIKeyEnabled(c.Config) {
|
||||
driver, err := repodbfactory.New(c.Config.Storage.StorageConfig, c.Log) //nolint:contextcheck
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -20,11 +20,14 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v52/github"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
|
||||
"github.com/zitadel/oidc/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/pkg/oidc"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
|
@ -55,13 +58,38 @@ func NewRouteHandler(c *Controller) *RouteHandler {
|
|||
}
|
||||
|
||||
func (rh *RouteHandler) SetupRoutes() {
|
||||
// first get Auth middleware in order to first setup openid/ldap/htpasswd, before oidc provider routes are setup
|
||||
authHandler := AuthHandler(rh.c)
|
||||
|
||||
applyCORSHeaders := getCORSHeadersHandler(rh.c.Config.HTTP.AllowOrigin)
|
||||
|
||||
if isOpenIDAuthEnabled(rh.c.Config) {
|
||||
// login path for openID
|
||||
rh.c.Router.HandleFunc(constants.LoginPath, rh.AuthURLHandler())
|
||||
|
||||
// logout path for openID
|
||||
rh.c.Router.HandleFunc(constants.LogoutPath, applyCORSHeaders(rh.Logout)).
|
||||
Methods(zcommon.AllowedMethods("POST")...)
|
||||
|
||||
// callback path for openID
|
||||
for provider, relyingParty := range rh.c.RelyingParties {
|
||||
if IsOauth2Supported(provider) {
|
||||
rh.c.Router.HandleFunc(constants.CallbackBasePath+fmt.Sprintf("/%s", provider),
|
||||
rp.CodeExchangeHandler(rh.GithubCodeExchangeCallback(), relyingParty))
|
||||
} else if IsOpenIDSupported(provider) {
|
||||
rh.c.Router.HandleFunc(constants.CallbackBasePath+fmt.Sprintf("/%s", provider),
|
||||
rp.CodeExchangeHandler(rp.UserinfoCallback(rh.OpenIDCodeExchangeCallback()), relyingParty))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prefixedRouter := rh.c.Router.PathPrefix(constants.RoutePrefix).Subrouter()
|
||||
prefixedRouter.Use(AuthHandler(rh.c))
|
||||
prefixedRouter.Use(authHandler)
|
||||
|
||||
prefixedDistSpecRouter := prefixedRouter.NewRoute().Subrouter()
|
||||
// authz is being enabled if AccessControl is specified
|
||||
// if Authn is not present AccessControl will have only default policies
|
||||
if rh.c.Config.HTTP.AccessControl != nil && !isBearerAuthEnabled(rh.c.Config) {
|
||||
if rh.c.Config.HTTP.AccessControl != nil {
|
||||
if isAuthnEnabled(rh.c.Config) {
|
||||
rh.c.Log.Info().Msg("access control is being enabled")
|
||||
} else {
|
||||
|
@ -72,8 +100,6 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
prefixedDistSpecRouter.Use(DistSpecAuthzHandler(rh.c))
|
||||
}
|
||||
|
||||
applyCORSHeaders := getCORSHeadersHandler(rh.c.Config.HTTP.AllowOrigin)
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#endpoints
|
||||
{
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", zreg.NameRegexp.String()),
|
||||
|
@ -118,7 +144,7 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
constants.ArtifactSpecRoutePrefix, zreg.NameRegexp.String()), rh.GetOrasReferrers).Methods("GET")
|
||||
|
||||
// swagger
|
||||
debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, AuthHandler(rh.c), rh.c.Log)
|
||||
debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
|
||||
|
||||
// Setup Extensions Routes
|
||||
if rh.c.Config != nil {
|
||||
|
@ -135,8 +161,8 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
rh.c.Log)
|
||||
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.RepoDB,
|
||||
rh.c.CveInfo, rh.c.Log)
|
||||
|
||||
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, AuthHandler(rh.c), rh.c.Log)
|
||||
ext.SetupAPIKeyRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.RepoDB, rh.c.CookieStore, rh.c.Log)
|
||||
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, authHandler, rh.c.Log)
|
||||
|
||||
gqlPlayground.SetupGQLPlaygroundRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.Log)
|
||||
|
||||
|
@ -185,7 +211,8 @@ func addCORSHeaders(allowOrigin string, response http.ResponseWriter) {
|
|||
// @Success 200 {string} string "ok".
|
||||
func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -195,10 +222,13 @@ func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, reques
|
|||
// NOTE: compatibility workaround - return this header in "allowed-read" mode to allow for clients to
|
||||
// work correctly
|
||||
if rh.c.Config.HTTP.Auth != nil {
|
||||
if rh.c.Config.HTTP.Auth.Bearer != nil {
|
||||
response.Header().Set("WWW-Authenticate", fmt.Sprintf("bearer realm=%s", rh.c.Config.HTTP.Auth.Bearer.Realm))
|
||||
} else {
|
||||
response.Header().Set("WWW-Authenticate", fmt.Sprintf("basic realm=%s", rh.c.Config.HTTP.Realm))
|
||||
// don't send auth headers if request is coming from UI
|
||||
if request.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
|
||||
if rh.c.Config.HTTP.Auth.Bearer != nil {
|
||||
response.Header().Set("WWW-Authenticate", fmt.Sprintf("bearer realm=%s", rh.c.Config.HTTP.Auth.Bearer.Realm))
|
||||
} else {
|
||||
response.Header().Set("WWW-Authenticate", fmt.Sprintf("basic realm=%s", rh.c.Config.HTTP.Realm))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,7 +254,8 @@ type ImageTags struct {
|
|||
// @Failure 400 {string} string "bad request".
|
||||
func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -355,7 +386,8 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req
|
|||
// @Failure 500 {string} string "internal server error".
|
||||
func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -427,7 +459,8 @@ type ExtensionList struct {
|
|||
// @Router /v2/{name}/manifests/{reference} [get].
|
||||
func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -527,7 +560,8 @@ func getReferrers(routeHandler *RouteHandler,
|
|||
// @Router /v2/{name}/referrers/{digest} [get].
|
||||
func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -1576,7 +1610,8 @@ type RepositoryList struct {
|
|||
// @Router /v2/_catalog [get].
|
||||
func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -1642,7 +1677,8 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
|
|||
// @Router /v2/_oci/ext/discover [get].
|
||||
func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -1653,6 +1689,116 @@ func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) {
|
|||
zcommon.WriteJSON(w, http.StatusOK, extensionList)
|
||||
}
|
||||
|
||||
// The following routes are specific to zot and NOT part of the OCI dist-spec
|
||||
|
||||
// Logout godoc
|
||||
// @Summary Logout by removing current session
|
||||
// @Description Logout by removing current session
|
||||
// @Router /openid/auth/logout [post]
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "ok".
|
||||
// @Failure 500 {string} string "internal server error".
|
||||
func (rh *RouteHandler) Logout(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
response.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := rh.c.CookieStore.Get(request, "session")
|
||||
session.Options.MaxAge = -1
|
||||
|
||||
err := session.Save(request, response)
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// github Oauth2 CodeExchange callback.
|
||||
func (rh *RouteHandler) GithubCodeExchangeCallback() rp.CodeExchangeCallback {
|
||||
return func(w http.ResponseWriter, r *http.Request,
|
||||
tokens *oidc.Tokens, state string, relyingParty rp.RelyingParty,
|
||||
) {
|
||||
ctx := r.Context()
|
||||
|
||||
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, tokens.Token))
|
||||
|
||||
email, groups, err := GetGithubUserInfo(ctx, client, rh.c.Log)
|
||||
if email == "" || err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
callbackUI, err := OAuth2Callback(rh.c, w, r, state, email, groups) //nolint: contextcheck
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrInvalidStateCookie) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if callbackUI != "" {
|
||||
http.Redirect(w, r, callbackUI, http.StatusFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// Openid CodeExchange callback.
|
||||
func (rh *RouteHandler) OpenIDCodeExchangeCallback() rp.CodeExchangeUserinfoCallback {
|
||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string,
|
||||
relyingParty rp.RelyingParty, info oidc.UserInfo,
|
||||
) {
|
||||
email := info.GetEmail()
|
||||
if email == "" {
|
||||
rh.c.Log.Error().Msg("couldn't set user record for empty email value")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var groups []string
|
||||
|
||||
val, ok := info.GetClaim("groups").([]interface{})
|
||||
if !ok {
|
||||
rh.c.Log.Info().Msgf("couldn't find any 'groups' claim for user %s", email)
|
||||
}
|
||||
|
||||
for _, group := range val {
|
||||
groups = append(groups, fmt.Sprint(group))
|
||||
}
|
||||
|
||||
callbackUI, err := OAuth2Callback(rh.c, w, r, state, email, groups)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrInvalidStateCookie) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if callbackUI != "" {
|
||||
http.Redirect(w, r, callbackUI, http.StatusFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *RouteHandler) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
m := rh.c.Metrics.ReceiveMetrics()
|
||||
zcommon.WriteJSON(w, http.StatusOK, m)
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
//go:build sync && scrub && metrics && search && lint
|
||||
// +build sync,scrub,metrics,search,lint
|
||||
//go:build sync && scrub && metrics && search && lint && apikey
|
||||
// +build sync,scrub,metrics,search,lint,apikey
|
||||
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/project-zot/mockoidc"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/zitadel/oidc/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageTypes "zotregistry.io/zot/pkg/storage/types"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
|
@ -29,6 +39,8 @@ import (
|
|||
|
||||
var ErrUnexpectedError = errors.New("error: unexpected error")
|
||||
|
||||
const sessionStr = "session"
|
||||
|
||||
func TestRoutes(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := test.GetFreePort()
|
||||
|
@ -36,6 +48,45 @@ func TestRoutes(t *testing.T) {
|
|||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
defer os.Remove(htpasswdPath)
|
||||
mockOIDCServer, err := mockoidc.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
err := mockOIDCServer.Shutdown()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
},
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"dex": {
|
||||
ClientID: mockOIDCConfig.ClientID,
|
||||
ClientSecret: mockOIDCConfig.ClientSecret,
|
||||
KeyPath: "",
|
||||
Issuer: mockOIDCConfig.Issuer,
|
||||
Scopes: []string{"openid", "email"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultVal := true
|
||||
apiKeyConfig := &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: apiKeyConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
@ -50,6 +101,52 @@ func TestRoutes(t *testing.T) {
|
|||
// NOTE: the url or method itself doesn't matter below since we are calling the handlers directly,
|
||||
// so path routing is bypassed
|
||||
|
||||
Convey("Test GithubCodeExchangeCallback", func() {
|
||||
callback := rthdlr.GithubCodeExchangeCallback()
|
||||
ctx := context.TODO()
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
tokens := &oidc.Tokens{}
|
||||
relyingParty, err := rp.NewRelyingPartyOAuth(&oauth2.Config{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
callback(response, request, tokens, "state", relyingParty)
|
||||
|
||||
resp := response.Result()
|
||||
defer resp.Body.Close()
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
Convey("Test OAuth2Callback errors", func() {
|
||||
ctx := context.TODO()
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
_, err := api.OAuth2Callback(ctlr, response, request, "state", "email", []string{"group"})
|
||||
So(err, ShouldEqual, zerr.ErrInvalidStateCookie)
|
||||
|
||||
session, _ := ctlr.CookieStore.Get(request, "statecookie")
|
||||
|
||||
session.Options.Secure = true
|
||||
session.Options.HttpOnly = true
|
||||
session.Options.SameSite = http.SameSiteDefaultMode
|
||||
|
||||
state := uuid.New().String()
|
||||
|
||||
session.Values["state"] = state
|
||||
|
||||
// let the session set its own id
|
||||
err = session.Save(request, response)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = api.OAuth2Callback(ctlr, response, request, "state", "email", []string{"group"})
|
||||
So(err, ShouldEqual, zerr.ErrInvalidStateCookie)
|
||||
})
|
||||
|
||||
Convey("List repositories authz error", func() {
|
||||
var invalid struct{}
|
||||
|
||||
|
@ -575,7 +672,7 @@ func TestRoutes(t *testing.T) {
|
|||
},
|
||||
&mocks.MockedImageStore{
|
||||
FullBlobUploadFn: func(repo string, body io.Reader, digest godigest.Digest) (string, int64, error) {
|
||||
return "session", 0, zerr.ErrBadBlobDigest
|
||||
return sessionStr, 0, zerr.ErrBadBlobDigest
|
||||
},
|
||||
})
|
||||
So(statusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
|
@ -591,7 +688,7 @@ func TestRoutes(t *testing.T) {
|
|||
},
|
||||
&mocks.MockedImageStore{
|
||||
FullBlobUploadFn: func(repo string, body io.Reader, digest godigest.Digest) (string, int64, error) {
|
||||
return "session", 20, nil
|
||||
return sessionStr, 20, nil
|
||||
},
|
||||
})
|
||||
So(statusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
|
@ -1327,6 +1424,80 @@ func TestRoutes(t *testing.T) {
|
|||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
Convey("Test API keys", func() {
|
||||
var invalid struct{}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, invalid)
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
extensions.CreateAPIKey(response, request, ctlr.RepoDB, ctlr.CookieStore, ctlr.Log)
|
||||
|
||||
resp := response.Result()
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: username,
|
||||
}
|
||||
|
||||
ctx = context.TODO()
|
||||
key = localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, acCtx)
|
||||
|
||||
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
|
||||
response = httptest.NewRecorder()
|
||||
|
||||
extensions.CreateAPIKey(response, request, ctlr.RepoDB, ctlr.CookieStore, ctlr.Log)
|
||||
|
||||
resp = response.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
payload := extensions.APIKeyPayload{
|
||||
Label: "test",
|
||||
Scopes: []string{"test"},
|
||||
}
|
||||
reqBody, err := json.Marshal(payload)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(reqBody))
|
||||
response = httptest.NewRecorder()
|
||||
|
||||
extensions.CreateAPIKey(response, request, mocks.RepoDBMock{
|
||||
AddUserAPIKeyFn: func(ctx context.Context, hashedKey string, apiKeyDetails *repodb.APIKeyDetails) error {
|
||||
return ErrUnexpectedError
|
||||
},
|
||||
}, ctlr.CookieStore, ctlr.Log)
|
||||
|
||||
resp = response.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
request, _ = http.NewRequestWithContext(ctx, http.MethodDelete, baseURL, bytes.NewReader([]byte{}))
|
||||
response = httptest.NewRecorder()
|
||||
|
||||
q := request.URL.Query()
|
||||
q.Add("id", "apikeyid")
|
||||
request.URL.RawQuery = q.Encode()
|
||||
|
||||
extensions.RevokeAPIKey(response, request, mocks.RepoDBMock{
|
||||
DeleteUserAPIKeyFn: func(ctx context.Context, id string) error {
|
||||
return ErrUnexpectedError
|
||||
},
|
||||
}, ctlr.CookieStore, ctlr.Log)
|
||||
|
||||
resp = response.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
Convey("Helper functions", func() {
|
||||
testUpdateBlobUpload := func(
|
||||
query []struct{ k, v string },
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build sync && scrub && metrics && search
|
||||
// +build sync,scrub,metrics,search
|
||||
//go:build sync && scrub && metrics && search && apikey
|
||||
// +build sync,scrub,metrics,search,apikey
|
||||
|
||||
package cli_test
|
||||
|
||||
|
@ -857,6 +857,67 @@ func TestServeMgmtExtension(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestServeAPIKeyExtension(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("apikey implicitly enabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":true}")
|
||||
})
|
||||
|
||||
Convey("apikey disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": "false"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":false}")
|
||||
})
|
||||
}
|
||||
|
||||
func readLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { //nolint:unparam,lll
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancelFunc()
|
||||
|
|
|
@ -361,6 +361,10 @@ func validateConfiguration(config *config.Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := validateOpenIDConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateSync(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -377,7 +381,7 @@ func validateConfiguration(config *config.Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// check authorization config, it should have basic auth enabled or ldap
|
||||
// check authorization config, it should have basic auth enabled or ldap, api keys or OpenID
|
||||
if config.HTTP.AccessControl != nil {
|
||||
// checking for anonymous policy only authorization config: no users, no policies but anonymous policy
|
||||
if err := validateAuthzPolicies(config); err != nil {
|
||||
|
@ -435,11 +439,42 @@ func validateConfiguration(config *config.Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateOpenIDConfig(config *config.Config) error {
|
||||
if config.HTTP.Auth != nil && config.HTTP.Auth.OpenID != nil {
|
||||
for provider, providerConfig := range config.HTTP.Auth.OpenID.Providers {
|
||||
//nolint: gocritic
|
||||
if api.IsOpenIDSupported(provider) {
|
||||
if providerConfig.ClientID == "" || providerConfig.Issuer == "" ||
|
||||
len(providerConfig.Scopes) == 0 {
|
||||
log.Error().Err(errors.ErrBadConfig).
|
||||
Msg("OpenID provider config requires clientid, issuer and scopes parameters")
|
||||
|
||||
return errors.ErrBadConfig
|
||||
}
|
||||
} else if api.IsOauth2Supported(provider) {
|
||||
if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 {
|
||||
log.Error().Err(errors.ErrBadConfig).
|
||||
Msg("OAuth2 provider config requires clientid and scopes parameters")
|
||||
|
||||
return errors.ErrBadConfig
|
||||
}
|
||||
} else {
|
||||
log.Error().Err(errors.ErrBadConfig).
|
||||
Msg("unsupported openid/oauth2 provider")
|
||||
|
||||
return errors.ErrBadConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAuthzPolicies(config *config.Config) error {
|
||||
if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil)) &&
|
||||
!authzContainsOnlyAnonymousPolicy(config) {
|
||||
if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil &&
|
||||
config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) {
|
||||
log.Error().Err(errors.ErrBadConfig).
|
||||
Msg("access control config requires httpasswd, ldap authentication " +
|
||||
Msg("access control config requires one of httpasswd, ldap or openid authentication " +
|
||||
"or using only 'anonymousPolicy' policies")
|
||||
|
||||
return errors.ErrBadConfig
|
||||
|
@ -484,6 +519,13 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Mgmt = &extconf.MgmtConfig{}
|
||||
}
|
||||
|
||||
_, ok = extMap["apikey"]
|
||||
if ok {
|
||||
// we found a config like `"extensions": {"mgmt:": {}}`
|
||||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.APIKey = &extconf.APIKeyConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions != nil {
|
||||
|
@ -550,6 +592,12 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
}
|
||||
}
|
||||
|
||||
if config.Extensions.APIKey != nil {
|
||||
if config.Extensions.APIKey.Enable == nil {
|
||||
config.Extensions.APIKey.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Scrub != nil {
|
||||
if config.Extensions.Scrub.Enable == nil {
|
||||
config.Extensions.Scrub.Enable = &defaultVal
|
||||
|
|
|
@ -952,6 +952,71 @@ func TestVerify(t *testing.T) {
|
|||
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
|
||||
})
|
||||
|
||||
Convey("Test verify openid config with missing parameter", t, func(c C) {
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex"}}}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
_, err = tmpfile.Write(content)
|
||||
So(err, ShouldBeNil)
|
||||
err = tmpfile.Close()
|
||||
So(err, ShouldBeNil)
|
||||
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
|
||||
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
|
||||
})
|
||||
|
||||
Convey("Test verify oauth2 config with missing parameter", t, func(c C) {
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"openid":{"providers":{"github":{"clientid":"client_id"}}}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
_, err = tmpfile.Write(content)
|
||||
So(err, ShouldBeNil)
|
||||
err = tmpfile.Close()
|
||||
So(err, ShouldBeNil)
|
||||
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
|
||||
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
|
||||
})
|
||||
|
||||
Convey("Test verify openid config with unsupported provider", t, func(c C) {
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"openid":{"providers":{"unsupported":{"issuer":"http://127.0.0.1:5556/dex"}}}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
_, err = tmpfile.Write(content)
|
||||
So(err, ShouldBeNil)
|
||||
err = tmpfile.Close()
|
||||
So(err, ShouldBeNil)
|
||||
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
|
||||
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
|
||||
})
|
||||
|
||||
Convey("Test verify openid config without apikey extension enabled", t, func(c C) {
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex",
|
||||
"clientid":"client_id","scopes":["openid"]}}}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
_, err = tmpfile.Write(content)
|
||||
So(err, ShouldBeNil)
|
||||
err = tmpfile.Close()
|
||||
So(err, ShouldBeNil)
|
||||
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
|
||||
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldNotPanic)
|
||||
})
|
||||
|
||||
Convey("Test verify config with missing basedn key", t, func(c C) {
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
|
|
@ -2,14 +2,17 @@ package common
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
apiErr "zotregistry.io/zot/pkg/api/errors"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
)
|
||||
|
||||
func AllowedMethods(methods ...string) []string {
|
||||
|
@ -32,7 +35,8 @@ func ACHeadersHandler(allowedMethods ...string) mux.MiddlewareFunc {
|
|||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
resp.Header().Set("Access-Control-Allow-Methods", headerValue)
|
||||
resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
|
||||
resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
|
||||
resp.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if req.Method == http.MethodOptions {
|
||||
return
|
||||
|
@ -43,9 +47,20 @@ func ACHeadersHandler(allowedMethods ...string) mux.MiddlewareFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func AuthzFail(w http.ResponseWriter, realm string, delay int) {
|
||||
func AuthzFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
w.Header().Set("WWW-Authenticate", realm)
|
||||
|
||||
// don't send auth headers if request is coming from UI
|
||||
if r.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
|
||||
if realm == "" {
|
||||
realm = "Authorization Required"
|
||||
}
|
||||
|
||||
realm = "Basic realm=" + strconv.Quote(realm)
|
||||
|
||||
w.Header().Set("WWW-Authenticate", realm)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteJSON(w, http.StatusForbidden, apiErr.NewErrorList(apiErr.NewError(apiErr.DENIED)))
|
||||
}
|
||||
|
@ -66,3 +81,39 @@ func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte)
|
|||
w.WriteHeader(status)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
/*
|
||||
GetAuthUserFromRequestSession returns identity
|
||||
and auth status if on the request's cookie session is a logged in user.
|
||||
*/
|
||||
func GetAuthUserFromRequestSession(cookieStore sessions.Store, request *http.Request, log log.Logger,
|
||||
) (string, bool) {
|
||||
session, err := cookieStore.Get(request, "session")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("can not decode existing session")
|
||||
// expired cookie, no need to return err
|
||||
return "", false
|
||||
}
|
||||
|
||||
// at this point we should have a session set on cookie.
|
||||
// if created in the earlier Get() call then user is not logged in with sessions.
|
||||
if session.IsNew {
|
||||
return "", false
|
||||
}
|
||||
|
||||
authenticated := session.Values["authStatus"]
|
||||
if authenticated != true {
|
||||
log.Error().Msg("can not get `user` session value")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
identity, ok := session.Values["user"].(string)
|
||||
if !ok {
|
||||
log.Error().Msg("can not get `user` session value")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return identity, true
|
||||
}
|
||||
|
|
66
pkg/extensions/README_apikey.md
Normal file
66
pkg/extensions/README_apikey.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# `API keys`
|
||||
|
||||
zot allows authentication for REST API calls using your API key as an alternative to your password.
|
||||
|
||||
* User can create/revoke his API key.
|
||||
|
||||
* Can not be retrieved, it is shown to the user only the first time is created.
|
||||
|
||||
* An API key has the same rights as the user who generated it.
|
||||
|
||||
## API keys REST API
|
||||
|
||||
|
||||
### Create API Key
|
||||
**Description**: Create an API key for the current user.
|
||||
|
||||
**Usage**: POST /v2/_zot/ext/apikey
|
||||
|
||||
**Produces**: application/json
|
||||
|
||||
**Sample input**:
|
||||
```
|
||||
POST /api/security/apiKey
|
||||
Body: {"label": "git", "scopes": ["repo1", "repo2"]}'
|
||||
```
|
||||
|
||||
**Example cURL**
|
||||
```
|
||||
curl -u user:password -X POST http://localhost:8080/v2/_zot/ext/apikey -d '{"label": "myLabel", "scopes": ["repo1", "repo2"]}'
|
||||
```
|
||||
|
||||
**Sample output**:
|
||||
```json
|
||||
{
|
||||
"createdAt": "2023-05-05T15:39:28.420926+03:00",
|
||||
"creatorUa": "curl/7.68.0",
|
||||
"generatedBy": "manual",
|
||||
"lastUsed": "2023-05-05T15:39:28.4209282+03:00",
|
||||
"label": "git",
|
||||
"scopes": [
|
||||
"repo1",
|
||||
"repo2"
|
||||
],
|
||||
"uuid": "46a45ce7-5d92-498a-a9cb-9654b1da3da1",
|
||||
"apiKey": "zak_e77bcb9e9f634f1581756abbf9ecd269"
|
||||
}
|
||||
```
|
||||
|
||||
**Using API keys cURL**
|
||||
```
|
||||
curl -u user:zak_e77bcb9e9f634f1581756abbf9ecd269 http://localhost:8080/v2/_catalog
|
||||
```
|
||||
|
||||
|
||||
### Revoke API Key
|
||||
**Description**: Revokes one current user API key by api key UUID
|
||||
|
||||
**Usage**: DELETE /api/security/apiKey?id=$uuid
|
||||
|
||||
**Produces**: application/json
|
||||
|
||||
|
||||
**Example cURL**
|
||||
```
|
||||
curl -u user:password -X DELETE http://localhost:8080/v2/_zot/ext/apikey?id=46a45ce7-5d92-498a-a9cb-9654b1da3da1
|
||||
```
|
|
@ -8,6 +8,7 @@ Component | Endpoint | Description
|
|||
[`search`](search/search.md) | `/v2/_zot/ext/search` | efficient and enhanced registry search capabilities using graphQL backend
|
||||
[`mgmt`](mgmt.md) | `/v2/_zot/ext/mgmt` | config management
|
||||
[`userprefs`](userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences
|
||||
[`apikey`](README_apikey.md) | `/v2/_zot/ext/apikey` | user api keys management
|
||||
|
||||
|
||||
# References
|
||||
|
|
|
@ -19,6 +19,11 @@ type ExtensionConfig struct {
|
|||
Lint *LintConfig
|
||||
UI *UIConfig
|
||||
Mgmt *MgmtConfig
|
||||
APIKey *APIKeyConfig
|
||||
}
|
||||
|
||||
type APIKeyConfig struct {
|
||||
BaseConfig `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type MgmtConfig struct {
|
||||
|
|
197
pkg/extensions/extension_api_key.go
Normal file
197
pkg/extensions/extension_api_key.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
//go:build apikey
|
||||
// +build apikey
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
guuid "github.com/gofrs/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
zcommon "zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
)
|
||||
|
||||
func SetupAPIKeyRoutes(config *config.Config, router *mux.Router, repoDB repodb.RepoDB,
|
||||
cookieStore sessions.Store, log log.Logger,
|
||||
) {
|
||||
if config.Extensions.APIKey != nil && *config.Extensions.APIKey.Enable {
|
||||
log.Info().Msg("setting up api key routes")
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodPost, http.MethodDelete)
|
||||
|
||||
apiKeyRouter := router.PathPrefix(constants.ExtAPIKey).Subrouter()
|
||||
apiKeyRouter.Use(zcommon.ACHeadersHandler(allowedMethods...))
|
||||
apiKeyRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
apiKeyRouter.Methods(allowedMethods...).Handler(HandleAPIKeyRequest(repoDB, cookieStore, log))
|
||||
}
|
||||
}
|
||||
|
||||
type APIKeyPayload struct { //nolint:revive
|
||||
Label string `json:"label"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
func HandleAPIKeyRequest(repoDB repodb.RepoDB, cookieStore sessions.Store,
|
||||
log log.Logger,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodPost:
|
||||
CreateAPIKey(resp, req, repoDB, cookieStore, log) //nolint:contextcheck
|
||||
|
||||
return
|
||||
case http.MethodDelete:
|
||||
RevokeAPIKey(resp, req, repoDB, cookieStore, log) //nolint:contextcheck
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CreateAPIKey godoc
|
||||
// @Summary Create an API key for the current user
|
||||
// @Description Can create an api key for a logged in user, based on the provided label and scopes.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 201 {string} string "created"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /v2/_zot/ext/apikey [post].
|
||||
func CreateAPIKey(resp http.ResponseWriter, req *http.Request, repoDB repodb.RepoDB,
|
||||
cookieStore sessions.Store, log log.Logger,
|
||||
) {
|
||||
var payload APIKeyPayload
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.Error().Msg("unable to read request body")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to unmarshal body")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiKeyBase, err := guuid.NewV4()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to generate uuid")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := strings.ReplaceAll(apiKeyBase.String(), "-", "")
|
||||
|
||||
hashedAPIKey := hashUUID(apiKey)
|
||||
|
||||
// will be used for identifying a specific api key
|
||||
apiKeyID, err := guuid.NewV4()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to generate uuid")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiKeyDetails := &repodb.APIKeyDetails{
|
||||
CreatedAt: time.Now(),
|
||||
LastUsed: time.Now(),
|
||||
CreatorUA: req.UserAgent(),
|
||||
GeneratedBy: "manual",
|
||||
Label: payload.Label,
|
||||
Scopes: payload.Scopes,
|
||||
UUID: apiKeyID.String(),
|
||||
}
|
||||
|
||||
err = repoDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error storing API key")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiKeyResponse := struct {
|
||||
repodb.APIKeyDetails
|
||||
APIKey string `json:"apiKey"`
|
||||
}{
|
||||
APIKey: fmt.Sprintf("%s%s", constants.APIKeysPrefix, apiKey),
|
||||
APIKeyDetails: *apiKeyDetails,
|
||||
}
|
||||
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
data, err := json.Marshal(apiKeyResponse)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to marshal api key response")
|
||||
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp.Header().Set("Content-Type", constants.DefaultMediaType)
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
_, _ = resp.Write(data)
|
||||
}
|
||||
|
||||
// RevokeAPIKey godoc
|
||||
// @Summary Revokes one current user API key
|
||||
// @Description Revokes one current user API key based on given key ID
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "api token id (UUID)"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Router /v2/_zot/ext/apikey?id=UUID [delete].
|
||||
func RevokeAPIKey(resp http.ResponseWriter, req *http.Request, repoDB repodb.RepoDB,
|
||||
cookieStore sessions.Store, log log.Logger,
|
||||
) {
|
||||
ids, ok := req.URL.Query()["id"]
|
||||
if !ok || len(ids) != 1 {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
keyID := ids[0]
|
||||
|
||||
err := repoDB.DeleteUserAPIKey(req.Context(), keyID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("keyID", keyID).Msg("error deleting API key")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func hashUUID(uuid string) string {
|
||||
digester := sha256.New()
|
||||
digester.Write([]byte(uuid))
|
||||
|
||||
return godigest.NewDigestFromEncoded(godigest.SHA256, fmt.Sprintf("%x", digester.Sum(nil))).Encoded()
|
||||
}
|
20
pkg/extensions/extension_api_key_disabled.go
Normal file
20
pkg/extensions/extension_api_key_disabled.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
//go:build !apikey
|
||||
// +build !apikey
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
)
|
||||
|
||||
func SetupAPIKeyRoutes(config *config.Config, router *mux.Router, repoDB repodb.RepoDB,
|
||||
cookieStore sessions.Store, log log.Logger,
|
||||
) {
|
||||
log.Warn().Msg("skipping setting up API key routes because given zot binary doesn't include this feature," +
|
||||
"please build a binary that does so")
|
||||
}
|
531
pkg/extensions/extension_api_key_test.go
Normal file
531
pkg/extensions/extension_api_key_test.go
Normal file
|
@ -0,0 +1,531 @@
|
|||
//go:build apikey
|
||||
// +build apikey
|
||||
|
||||
package extensions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/project-zot/mockoidc"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
"zotregistry.io/zot/pkg/test/mocks"
|
||||
)
|
||||
|
||||
type (
|
||||
apiKeyResponse struct {
|
||||
repodb.APIKeyDetails
|
||||
APIKey string `json:"apiKey"`
|
||||
}
|
||||
)
|
||||
|
||||
var ErrUnexpectedError = errors.New("unexpected err")
|
||||
|
||||
func TestAPIKeys(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
defer os.Remove(htpasswdPath)
|
||||
|
||||
mockOIDCServer, err := test.MockOIDCRun()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := mockOIDCServer.Shutdown()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
},
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"dex": {
|
||||
ClientID: mockOIDCConfig.ClientID,
|
||||
ClientSecret: mockOIDCConfig.ClientSecret,
|
||||
KeyPath: "",
|
||||
Issuer: mockOIDCConfig.Issuer,
|
||||
Scopes: []string{"openid", "email", "groups"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conf.HTTP.AccessControl = &config.AccessControlConfig{}
|
||||
|
||||
defaultVal := true
|
||||
apiKeyConfig := &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: apiKeyConfig,
|
||||
Mgmt: mgmtConfg,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
dir := t.TempDir()
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = dir
|
||||
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
|
||||
cm.StartServer()
|
||||
defer cm.StopServer()
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
payload := extensions.APIKeyPayload{
|
||||
Label: "test",
|
||||
Scopes: []string{"test"},
|
||||
}
|
||||
reqBody, err := json.Marshal(payload)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("API key retrieved with basic auth", func() {
|
||||
// call endpoint with session ( added to client after previous request)
|
||||
resp, err := resty.R().
|
||||
SetBody(reqBody).
|
||||
SetBasicAuth("test", "test").
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
user := mockoidc.DefaultUser()
|
||||
|
||||
// get API key and email from apikey route response
|
||||
var apiKeyResponse apiKeyResponse
|
||||
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
email := user.Email
|
||||
So(email, ShouldNotBeEmpty)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetBasicAuth("test", apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// add another one
|
||||
resp, err = resty.R().
|
||||
SetBody(reqBody).
|
||||
SetBasicAuth("test", "test").
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetBasicAuth("test", apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
Convey("API key retrieved with openID", func() {
|
||||
client := resty.New()
|
||||
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
||||
|
||||
// first login user
|
||||
resp, err := client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
SetQueryParam("provider", "dex").
|
||||
Get(baseURL + constants.LoginPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
|
||||
cookies := resp.Cookies()
|
||||
|
||||
// call endpoint without session
|
||||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
client.SetCookies(cookies)
|
||||
|
||||
// call endpoint with session ( added to client after previous request)
|
||||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
user := mockoidc.DefaultUser()
|
||||
|
||||
// get API key and email from apikey route response
|
||||
var apiKeyResponse apiKeyResponse
|
||||
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
email := user.Email
|
||||
So(email, ShouldNotBeEmpty)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// trigger errors
|
||||
ctlr.RepoDB = mocks.RepoDBMock{
|
||||
GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
|
||||
return "", ErrUnexpectedError
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
ctlr.RepoDB = mocks.RepoDBMock{
|
||||
GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
|
||||
return user.Email, nil
|
||||
},
|
||||
GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
|
||||
return []string{}, ErrUnexpectedError
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
ctlr.RepoDB = mocks.RepoDBMock{
|
||||
GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
|
||||
return user.Email, nil
|
||||
},
|
||||
UpdateUserAPIKeyLastUsedFn: func(ctx context.Context, hashedKey string) error {
|
||||
return ErrUnexpectedError
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
client = resty.New()
|
||||
|
||||
// call endpoint without session
|
||||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
Convey("Login with openid and create API key", func() {
|
||||
client := resty.New()
|
||||
|
||||
// mgmt should work both unauthenticated and authenticated
|
||||
resp, err := client.R().
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
||||
// first login user
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
SetQueryParam("provider", "dex").
|
||||
Get(baseURL + constants.LoginPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
client.SetCookies(resp.Cookies())
|
||||
|
||||
// call endpoint with session ( added to client after previous request)
|
||||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
var apiKeyResponse apiKeyResponse
|
||||
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
user := mockoidc.DefaultUser()
|
||||
email := user.Email
|
||||
So(email, ShouldNotBeEmpty)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// auth with API key
|
||||
// we need new client without session cookie set
|
||||
client = resty.New()
|
||||
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// invalid api keys
|
||||
resp, err = client.R().
|
||||
SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, "noprefixAPIKey").
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, "zak_notworkingAPIKey").
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: email,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = ctlr.RepoDB.DeleteUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
client = resty.New()
|
||||
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
||||
|
||||
// without creds should work
|
||||
resp, err = client.R().
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// login again
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
SetQueryParam("provider", "dex").
|
||||
Get(baseURL + constants.LoginPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
client.SetCookies(resp.Cookies())
|
||||
|
||||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// should work with session
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// should work with api key
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// delete api key
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
SetQueryParam("id", apiKeyResponse.UUID).
|
||||
Delete(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Delete(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth("test", "test").
|
||||
SetQueryParam("id", apiKeyResponse.UUID).
|
||||
Delete(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// unsupported method
|
||||
resp, err = client.R().
|
||||
Put(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIKeysOpenDBError(t *testing.T) {
|
||||
Convey("Test API keys - unable to create database", t, func() {
|
||||
conf := config.New()
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
defer os.Remove(htpasswdPath)
|
||||
|
||||
mockOIDCServer, err := test.MockOIDCRun()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := mockOIDCServer.Shutdown()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
},
|
||||
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"dex": {
|
||||
ClientID: mockOIDCConfig.ClientID,
|
||||
ClientSecret: mockOIDCConfig.ClientSecret,
|
||||
KeyPath: "",
|
||||
Issuer: mockOIDCConfig.Issuer,
|
||||
Scopes: []string{"openid", "email"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultVal := true
|
||||
apiKeyConfig := &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: apiKeyConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
dir := t.TempDir()
|
||||
|
||||
err = os.Chmod(dir, 0o000)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = dir
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
|
||||
So(func() {
|
||||
cm.StartServer()
|
||||
}, ShouldPanic)
|
||||
})
|
||||
}
|
|
@ -36,12 +36,19 @@ type BearerConfig struct {
|
|||
Service string `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
type OpenIDProviderConfig struct{}
|
||||
|
||||
type OpenIDConfig struct {
|
||||
Providers map[string]OpenIDProviderConfig `json:"providers,omitempty" mapstructure:"providers"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
HTPasswd *HTPasswd `json:"htpasswd,omitempty" mapstructure:"htpasswd"`
|
||||
Bearer *BearerConfig `json:"bearer,omitempty" mapstructure:"bearer"`
|
||||
LDAP *struct {
|
||||
Address string `json:"address,omitempty" mapstructure:"address"`
|
||||
} `json:"ldap,omitempty" mapstructure:"ldap"`
|
||||
OpenID *OpenIDConfig `json:"openid,omitempty" mapstructure:"openid"`
|
||||
}
|
||||
|
||||
type StrippedConfig struct {
|
||||
|
@ -60,8 +67,10 @@ func (auth Auth) MarshalJSON() ([]byte, error) {
|
|||
type localAuth Auth
|
||||
|
||||
if auth.Bearer == nil && auth.LDAP == nil &&
|
||||
auth.HTPasswd.Path == "" {
|
||||
auth.HTPasswd.Path == "" &&
|
||||
(auth.OpenID == nil || len(auth.OpenID.Providers) == 0) {
|
||||
auth.HTPasswd = nil
|
||||
auth.OpenID = nil
|
||||
|
||||
return json.Marshal((localAuth)(auth))
|
||||
}
|
||||
|
@ -72,6 +81,10 @@ func (auth Auth) MarshalJSON() ([]byte, error) {
|
|||
auth.HTPasswd.Path = ""
|
||||
}
|
||||
|
||||
if auth.OpenID != nil && len(auth.OpenID.Providers) == 0 {
|
||||
auth.OpenID = nil
|
||||
}
|
||||
|
||||
auth.LDAP = nil
|
||||
|
||||
return json.Marshal((localAuth)(auth))
|
||||
|
|
|
@ -39,6 +39,7 @@ func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, store
|
|||
userprefsRouter := router.PathPrefix(constants.ExtUserPreferences).Subrouter()
|
||||
userprefsRouter.Use(zcommon.ACHeadersHandler(allowedMethods...))
|
||||
userprefsRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
|
||||
userprefsRouter.HandleFunc("", HandleUserPrefs(repoDB, log)).Methods(allowedMethods...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build sync || metrics || mgmt
|
||||
// +build sync metrics mgmt
|
||||
//go:build sync || metrics || mgmt || apikey
|
||||
// +build sync metrics mgmt apikey
|
||||
|
||||
package extensions_test
|
||||
|
||||
|
@ -128,6 +128,20 @@ func TestMgmtExtension(t *testing.T) {
|
|||
|
||||
defaultValue := true
|
||||
|
||||
mockOIDCServer, err := test.MockOIDCRun()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := mockOIDCServer.Shutdown()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
|
@ -145,7 +159,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
@ -158,6 +172,13 @@ func TestMgmtExtension(t *testing.T) {
|
|||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
Convey("unsupported http method call", func() {
|
||||
// without credentials
|
||||
resp, err := resty.R().Patch(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
||||
})
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -210,9 +231,9 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
|
@ -259,9 +280,9 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
|
@ -325,11 +346,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
|
@ -396,9 +413,9 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
|
@ -445,11 +462,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
|
@ -474,6 +487,110 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with openID", t, func() {
|
||||
conf.HTTP.Auth.HTPasswd.Path = ""
|
||||
conf.HTTP.Auth.LDAP = nil
|
||||
conf.HTTP.Auth.Bearer = nil
|
||||
|
||||
openIDProviders := make(map[string]config.OpenIDProviderConfig)
|
||||
openIDProviders["dex"] = config.OpenIDProviderConfig{
|
||||
ClientID: mockOIDCConfig.ClientID,
|
||||
ClientSecret: mockOIDCConfig.ClientSecret,
|
||||
Issuer: mockOIDCConfig.Issuer,
|
||||
}
|
||||
|
||||
conf.HTTP.Auth.OpenID = &config.OpenIDConfig{
|
||||
Providers: openIDProviders,
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
mgmtResp := extensions.StrippedConfig{}
|
||||
err = json.Unmarshal(resp.Body(), &mgmtResp)
|
||||
t.Logf("resp: %v", mgmtResp.HTTP.Auth.OpenID)
|
||||
So(err, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.OpenID, ShouldNotBeNil)
|
||||
So(mgmtResp.HTTP.Auth.OpenID.Providers, ShouldNotBeEmpty)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with empty openID provider list", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
conf.HTTP.Auth.LDAP = nil
|
||||
conf.HTTP.Auth.Bearer = nil
|
||||
|
||||
openIDProviders := make(map[string]config.OpenIDProviderConfig)
|
||||
|
||||
conf.HTTP.Auth.OpenID = &config.OpenIDConfig{
|
||||
Providers: openIDProviders,
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
mgmtResp := extensions.StrippedConfig{}
|
||||
err = json.Unmarshal(resp.Body(), &mgmtResp)
|
||||
t.Logf("resp: %v", mgmtResp.HTTP.Auth.OpenID)
|
||||
So(err, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
|
||||
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.OpenID, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled without any auth", t, func() {
|
||||
globalDir := t.TempDir()
|
||||
conf := config.New()
|
||||
|
@ -499,11 +616,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
subPaths := make(map[string]config.StorageConfig)
|
||||
subPaths["/a"] = config.StorageConfig{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
|
@ -856,3 +969,32 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
|
|||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
|
||||
defaultVal := true
|
||||
|
||||
Convey("Test http options response", t, func() {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
conf.HTTP.Port = port
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
},
|
||||
}
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctrlManager := test.NewControllerManager(ctlr)
|
||||
|
||||
ctrlManager.StartAndWait(port)
|
||||
defer ctrlManager.StopServer()
|
||||
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,DELETE,OPTIONS")
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,6 +7,5 @@ const (
|
|||
RepoMetadataBucket = "RepoMetadata"
|
||||
UserDataBucket = "UserData"
|
||||
VersionBucket = "Version"
|
||||
StarredReposKey = "StarredReposKey"
|
||||
BookmarkedReposKey = "BookmarkedReposKey"
|
||||
UserAPIKeysBucket = "UserAPIKeys"
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
type DBDriverParameters struct {
|
||||
Endpoint, Region, RepoMetaTablename, ManifestDataTablename, IndexDataTablename,
|
||||
VersionTablename, UserDataTablename string
|
||||
UserDataTablename, APIKeyTablename, VersionTablename string
|
||||
}
|
||||
|
||||
func GetDynamoClient(params DBDriverParameters) (*dynamodb.Client, error) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package bolt
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -60,6 +61,11 @@ func NewBoltDBWrapper(boltDB *bbolt.DB, log log.Logger) (*DBWrapper, error) {
|
|||
return err
|
||||
}
|
||||
|
||||
_, err = transaction.CreateBucketIfNotExists([]byte(bolt.UserAPIKeysBucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -1680,39 +1686,26 @@ func (bdw *DBWrapper) ToggleStarRepo(ctx context.Context, repo string) (repodb.T
|
|||
var res repodb.ToggleState
|
||||
|
||||
if err := bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen
|
||||
userdb := tx.Bucket([]byte(bolt.UserDataBucket))
|
||||
userBucket, err := userdb.CreateBucketIfNotExists([]byte(userid))
|
||||
if err != nil {
|
||||
// this is a serious failure
|
||||
return zerr.ErrUnableToCreateUserBucket
|
||||
var userData repodb.UserData
|
||||
|
||||
err := bdw.getUserData(userid, tx, &userData)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
mdata := userBucket.Get([]byte(bolt.StarredReposKey))
|
||||
unpacked := []string{}
|
||||
if mdata != nil {
|
||||
if err = json.Unmarshal(mdata, &unpacked); err != nil {
|
||||
return zerr.ErrInvalidOldUserStarredRepos
|
||||
}
|
||||
}
|
||||
|
||||
isRepoStarred := zcommon.Contains(unpacked, repo)
|
||||
isRepoStarred := zcommon.Contains(userData.StarredRepos, repo)
|
||||
|
||||
if isRepoStarred {
|
||||
res = repodb.Removed
|
||||
unpacked = zcommon.RemoveFrom(unpacked, repo)
|
||||
userData.StarredRepos = zcommon.RemoveFrom(userData.StarredRepos, repo)
|
||||
} else {
|
||||
res = repodb.Added
|
||||
unpacked = append(unpacked, repo)
|
||||
userData.StarredRepos = append(userData.StarredRepos, repo)
|
||||
}
|
||||
|
||||
var repacked []byte
|
||||
if repacked, err = json.Marshal(unpacked); err != nil {
|
||||
return zerr.ErrCouldNotMarshalStarredRepos
|
||||
}
|
||||
|
||||
err = userBucket.Put([]byte(bolt.StarredReposKey), repacked)
|
||||
err = bdw.setUserData(userid, tx, userData)
|
||||
if err != nil {
|
||||
return zerr.ErrCouldNotPersistData
|
||||
return err
|
||||
}
|
||||
|
||||
repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket))
|
||||
|
@ -1755,46 +1748,12 @@ func (bdw *DBWrapper) ToggleStarRepo(ctx context.Context, repo string) (repodb.T
|
|||
}
|
||||
|
||||
func (bdw *DBWrapper) GetStarredRepos(ctx context.Context) ([]string, error) {
|
||||
starredRepos := make([]string, 0)
|
||||
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return starredRepos, err
|
||||
userData, err := bdw.GetUserData(ctx)
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) || errors.Is(err, zerr.ErrUserDataNotAllowed) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
|
||||
err = bdw.DB.View(func(tx *bbolt.Tx) error { //nolint:dupl
|
||||
if userid == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
userdb := tx.Bucket([]byte(bolt.UserDataBucket))
|
||||
userBucket := userdb.Bucket([]byte(userid))
|
||||
|
||||
if userBucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
mdata := userBucket.Get([]byte(bolt.StarredReposKey))
|
||||
if mdata == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(mdata, &starredRepos); err != nil {
|
||||
bdw.Log.Info().Str("user", userid).Err(err).Msg("unmarshal error")
|
||||
|
||||
return zerr.ErrInvalidOldUserStarredRepos
|
||||
}
|
||||
|
||||
if starredRepos == nil {
|
||||
starredRepos = make([]string, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return starredRepos, err
|
||||
return userData.StarredRepos, err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) ToggleBookmarkRepo(ctx context.Context, repo string) (repodb.ToggleState, error) {
|
||||
|
@ -1815,43 +1774,25 @@ func (bdw *DBWrapper) ToggleBookmarkRepo(ctx context.Context, repo string) (repo
|
|||
|
||||
var res repodb.ToggleState
|
||||
|
||||
if err := bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:dupl
|
||||
userdb := tx.Bucket([]byte(bolt.UserDataBucket))
|
||||
userBucket, err := userdb.CreateBucketIfNotExists([]byte(userid))
|
||||
if err != nil {
|
||||
// this is a serious failure
|
||||
return zerr.ErrUnableToCreateUserBucket
|
||||
if err := bdw.DB.Update(func(transaction *bbolt.Tx) error { //nolint:dupl
|
||||
var userData repodb.UserData
|
||||
|
||||
err := bdw.getUserData(userid, transaction, &userData)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
mdata := userBucket.Get([]byte(bolt.BookmarkedReposKey))
|
||||
unpacked := []string{}
|
||||
if mdata != nil {
|
||||
if err = json.Unmarshal(mdata, &unpacked); err != nil {
|
||||
return zerr.ErrInvalidOldUserBookmarkedRepos
|
||||
}
|
||||
}
|
||||
|
||||
isRepoBookmarked := zcommon.Contains(unpacked, repo)
|
||||
isRepoBookmarked := zcommon.Contains(userData.BookmarkedRepos, repo)
|
||||
|
||||
if isRepoBookmarked {
|
||||
res = repodb.Removed
|
||||
unpacked = zcommon.RemoveFrom(unpacked, repo)
|
||||
userData.BookmarkedRepos = zcommon.RemoveFrom(userData.BookmarkedRepos, repo)
|
||||
} else {
|
||||
res = repodb.Added
|
||||
unpacked = append(unpacked, repo)
|
||||
userData.BookmarkedRepos = append(userData.BookmarkedRepos, repo)
|
||||
}
|
||||
|
||||
var repacked []byte
|
||||
if repacked, err = json.Marshal(unpacked); err != nil {
|
||||
return zerr.ErrCouldNotMarshalBookmarkedRepos
|
||||
}
|
||||
|
||||
err = userBucket.Put([]byte(bolt.BookmarkedReposKey), repacked)
|
||||
if err != nil {
|
||||
return zerr.ErrUnableToCreateUserBucket
|
||||
}
|
||||
|
||||
return nil
|
||||
return bdw.setUserData(userid, transaction, userData)
|
||||
}); err != nil {
|
||||
return repodb.NotChanged, err
|
||||
}
|
||||
|
@ -1860,46 +1801,12 @@ func (bdw *DBWrapper) ToggleBookmarkRepo(ctx context.Context, repo string) (repo
|
|||
}
|
||||
|
||||
func (bdw *DBWrapper) GetBookmarkedRepos(ctx context.Context) ([]string, error) {
|
||||
bookmarkedRepos := []string{}
|
||||
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return bookmarkedRepos, err
|
||||
userData, err := bdw.GetUserData(ctx)
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) || errors.Is(err, zerr.ErrUserDataNotAllowed) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
|
||||
err = bdw.DB.View(func(tx *bbolt.Tx) error { //nolint:dupl
|
||||
if userid == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
userdb := tx.Bucket([]byte(bolt.UserDataBucket))
|
||||
userBucket := userdb.Bucket([]byte(userid))
|
||||
|
||||
if userBucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
mdata := userBucket.Get([]byte(bolt.BookmarkedReposKey))
|
||||
if mdata == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(mdata, &bookmarkedRepos); err != nil {
|
||||
bdw.Log.Info().Str("user", userid).Err(err).Msg("unmarshal error")
|
||||
|
||||
return zerr.ErrInvalidOldUserBookmarkedRepos
|
||||
}
|
||||
|
||||
if bookmarkedRepos == nil {
|
||||
bookmarkedRepos = make([]string, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return bookmarkedRepos, err
|
||||
return userData.BookmarkedRepos, err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) PatchDB() error {
|
||||
|
@ -1940,30 +1847,25 @@ func getUserStars(ctx context.Context, transaction *bbolt.Tx) []string {
|
|||
}
|
||||
|
||||
var (
|
||||
userid = localCtx.GetUsernameFromContext(acCtx)
|
||||
starredRepos = []string{}
|
||||
userdb = transaction.Bucket([]byte(bolt.UserDataBucket))
|
||||
userBucket = userdb.Bucket([]byte(userid))
|
||||
userData repodb.UserData
|
||||
userid = localCtx.GetUsernameFromContext(acCtx)
|
||||
userdb = transaction.Bucket([]byte(bolt.UserDataBucket))
|
||||
)
|
||||
|
||||
if userid == "" {
|
||||
if userid == "" || userdb == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if userBucket == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
mdata := userBucket.Get([]byte(bolt.StarredReposKey))
|
||||
mdata := userdb.Get([]byte(userid))
|
||||
if mdata == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(mdata, &starredRepos); err != nil {
|
||||
if err := json.Unmarshal(mdata, &userData); err != nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return starredRepos
|
||||
return userData.StarredRepos
|
||||
}
|
||||
|
||||
func getUserBookmarks(ctx context.Context, transaction *bbolt.Tx) []string {
|
||||
|
@ -1973,28 +1875,309 @@ func getUserBookmarks(ctx context.Context, transaction *bbolt.Tx) []string {
|
|||
}
|
||||
|
||||
var (
|
||||
userid = localCtx.GetUsernameFromContext(acCtx)
|
||||
bookmarkedRepos = []string{}
|
||||
userdb = transaction.Bucket([]byte(bolt.UserDataBucket))
|
||||
userBucket = userdb.Bucket([]byte(userid))
|
||||
userData repodb.UserData
|
||||
userid = localCtx.GetUsernameFromContext(acCtx)
|
||||
userdb = transaction.Bucket([]byte(bolt.UserDataBucket))
|
||||
)
|
||||
|
||||
if userid == "" {
|
||||
if userid == "" || userdb == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if userBucket == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
mdata := userBucket.Get([]byte(bolt.BookmarkedReposKey))
|
||||
mdata := userdb.Get([]byte(userid))
|
||||
if mdata == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(mdata, &bookmarkedRepos); err != nil {
|
||||
if err := json.Unmarshal(mdata, &userData); err != nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return bookmarkedRepos
|
||||
return userData.BookmarkedRepos
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) SetUserGroups(ctx context.Context, groups []string) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen
|
||||
var userData repodb.UserData
|
||||
|
||||
err := bdw.getUserData(userid, tx, &userData)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
userData.Groups = append(userData.Groups, groups...)
|
||||
|
||||
err = bdw.setUserData(userid, tx, userData)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) GetUserGroups(ctx context.Context) ([]string, error) {
|
||||
userData, err := bdw.GetUserData(ctx)
|
||||
|
||||
return userData.Groups, err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen
|
||||
var userData repodb.UserData
|
||||
|
||||
err := bdw.getUserData(userid, tx, &userData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiKeyDetails := userData.APIKeys[hashedKey]
|
||||
apiKeyDetails.LastUsed = time.Now()
|
||||
|
||||
userData.APIKeys[hashedKey] = apiKeyDetails
|
||||
|
||||
err = bdw.setUserData(userid, tx, userData)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *repodb.APIKeyDetails) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.Update(func(transaction *bbolt.Tx) error {
|
||||
var userData repodb.UserData
|
||||
|
||||
apiKeysbuck := transaction.Bucket([]byte(bolt.UserAPIKeysBucket))
|
||||
if apiKeysbuck == nil {
|
||||
return zerr.ErrBucketDoesNotExist
|
||||
}
|
||||
|
||||
err := apiKeysbuck.Put([]byte(hashedKey), []byte(userid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("repoDB: error while setting userData for identity %s %w", userid, err)
|
||||
}
|
||||
|
||||
err = bdw.getUserData(userid, transaction, &userData)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if userData.APIKeys == nil {
|
||||
userData.APIKeys = make(map[string]repodb.APIKeyDetails)
|
||||
}
|
||||
|
||||
userData.APIKeys[hashedKey] = *apiKeyDetails
|
||||
|
||||
err = bdw.setUserData(userid, transaction, userData)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) DeleteUserAPIKey(ctx context.Context, keyID string) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.Update(func(transaction *bbolt.Tx) error {
|
||||
var userData repodb.UserData
|
||||
|
||||
apiKeysbuck := transaction.Bucket([]byte(bolt.UserAPIKeysBucket))
|
||||
if apiKeysbuck == nil {
|
||||
return zerr.ErrBucketDoesNotExist
|
||||
}
|
||||
|
||||
err := bdw.getUserData(userid, transaction, &userData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for hash, apiKeyDetails := range userData.APIKeys {
|
||||
if apiKeyDetails.UUID == keyID {
|
||||
delete(userData.APIKeys, hash)
|
||||
|
||||
err := apiKeysbuck.Delete([]byte(hash))
|
||||
if err != nil {
|
||||
return fmt.Errorf("userDB: error while deleting userAPIKey entry for hash %s %w", hash, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bdw.setUserData(userid, transaction, userData)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) GetUserAPIKeyInfo(hashedKey string) (string, error) {
|
||||
var userid string
|
||||
err := bdw.DB.View(func(tx *bbolt.Tx) error {
|
||||
buck := tx.Bucket([]byte(bolt.UserAPIKeysBucket))
|
||||
if buck == nil {
|
||||
return zerr.ErrBucketDoesNotExist
|
||||
}
|
||||
|
||||
uiBlob := buck.Get([]byte(hashedKey))
|
||||
if len(uiBlob) == 0 {
|
||||
return zerr.ErrUserAPIKeyNotFound
|
||||
}
|
||||
|
||||
userid = string(uiBlob)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return userid, err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) GetUserData(ctx context.Context) (repodb.UserData, error) {
|
||||
var userData repodb.UserData
|
||||
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return userData, err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return userData, zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.View(func(tx *bbolt.Tx) error {
|
||||
return bdw.getUserData(userid, tx, &userData)
|
||||
})
|
||||
|
||||
return userData, err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) getUserData(userid string, transaction *bbolt.Tx, res *repodb.UserData) error {
|
||||
buck := transaction.Bucket([]byte(bolt.UserDataBucket))
|
||||
if buck == nil {
|
||||
return zerr.ErrBucketDoesNotExist
|
||||
}
|
||||
|
||||
upBlob := buck.Get([]byte(userid))
|
||||
|
||||
if len(upBlob) == 0 {
|
||||
return zerr.ErrUserDataNotFound
|
||||
}
|
||||
|
||||
err := json.Unmarshal(upBlob, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) SetUserData(ctx context.Context, userData repodb.UserData) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return bdw.setUserData(userid, tx, userData)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) setUserData(userid string, transaction *bbolt.Tx, userData repodb.UserData) error {
|
||||
buck := transaction.Bucket([]byte(bolt.UserDataBucket))
|
||||
if buck == nil {
|
||||
return zerr.ErrBucketDoesNotExist
|
||||
}
|
||||
|
||||
upBlob, err := json.Marshal(userData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buck.Put([]byte(userid), upBlob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("repoDB: error while setting userData for identity %s %w", userid, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bdw *DBWrapper) DeleteUserData(ctx context.Context) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
err = bdw.DB.Update(func(tx *bbolt.Tx) error {
|
||||
buck := tx.Bucket([]byte(bolt.UserDataBucket))
|
||||
if buck == nil {
|
||||
return zerr.ErrBucketDoesNotExist
|
||||
}
|
||||
|
||||
err := buck.Delete([]byte(userid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("repoDB: error while deleting userData for identity %s %w", userid, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@ package bolt_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
@ -10,6 +13,7 @@ import (
|
|||
. "github.com/smartystreets/goconvey/convey"
|
||||
"go.etcd.io/bbolt"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/meta/bolt"
|
||||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
|
@ -21,7 +25,6 @@ import (
|
|||
|
||||
func TestWrapperErrors(t *testing.T) {
|
||||
Convey("Errors", t, func() {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
boltDBParams := bolt.DBParameters{RootDir: tmpDir}
|
||||
boltDriver, err := bolt.GetBoltDriver(boltDBParams)
|
||||
|
@ -41,6 +44,231 @@ func TestWrapperErrors(t *testing.T) {
|
|||
repoMetaBlob, err := json.Marshal(repoMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
Convey("AddUserAPIKey", func() {
|
||||
Convey("no userid found", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
err = boltdbWrapper.AddUserAPIKey(ctx, "", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
err = boltdbWrapper.AddUserAPIKey(ctx, "", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserDataBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.AddUserAPIKey(ctx, "test", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserAPIKeysBucket))
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.AddUserAPIKey(ctx, "", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldEqual, zerr.ErrBucketDoesNotExist)
|
||||
})
|
||||
|
||||
Convey("UpdateUserAPIKey", func() {
|
||||
err = boltdbWrapper.UpdateUserAPIKeyLastUsed(ctx, "")
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
err = boltdbWrapper.UpdateUserAPIKeyLastUsed(ctx, "") //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserAPIKey", func() {
|
||||
err = boltdbWrapper.SetUserData(ctx, repodb.UserData{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.AddUserAPIKey(ctx, "hashedKey", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("no such bucket", func() {
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserAPIKeysBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = boltdbWrapper.DeleteUserAPIKey(ctx, "")
|
||||
So(err, ShouldEqual, zerr.ErrBucketDoesNotExist)
|
||||
})
|
||||
|
||||
Convey("userdata not found", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
err := boltdbWrapper.DeleteUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.DeleteUserAPIKey(ctx, "")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx) //nolint: contextcheck
|
||||
|
||||
err = boltdbWrapper.DeleteUserAPIKey(ctx, "test") //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserDataBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.DeleteUserAPIKey(ctx, "") //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserAPIKeyInfo", func() {
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserAPIKeysBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.GetUserAPIKeyInfo("")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserData", func() {
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
buck := tx.Bucket([]byte(bolt.UserDataBucket))
|
||||
So(buck, ShouldNotBeNil)
|
||||
|
||||
return buck.Put([]byte("test"), []byte("dsa8"))
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserAPIKeysBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("SetUserData", func() {
|
||||
acCtx = localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = boltdbWrapper.SetUserData(ctx, repodb.UserData{})
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
buff := make([]byte, int(math.Ceil(float64(1000000)/float64(1.33333333333))))
|
||||
_, err := rand.Read(buff)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
longString := base64.RawURLEncoding.EncodeToString(buff)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: longString,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = boltdbWrapper.SetUserData(ctx, repodb.UserData{}) //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserDataBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
acCtx = localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = boltdbWrapper.SetUserData(ctx, repodb.UserData{}) //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserData", func() {
|
||||
acCtx = localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = boltdbWrapper.DeleteUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket([]byte(bolt.UserDataBucket))
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
acCtx = localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = boltdbWrapper.DeleteUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserGroups and SetUserGroups", func() {
|
||||
acCtx = localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
_, err := boltdbWrapper.GetUserGroups(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = boltdbWrapper.SetUserGroups(ctx, []string{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetManifestData", func() {
|
||||
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
dataBuck := tx.Bucket([]byte(bolt.ManifestDataBucket))
|
||||
|
@ -732,60 +960,6 @@ func TestWrapperErrors(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("ToggleStarRepo, getting StarredRepoKey from bucket fails", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
"repo": true,
|
||||
},
|
||||
Username: "username",
|
||||
}
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
userdb, err := tx.CreateBucketIfNotExists([]byte(bolt.UserDataBucket))
|
||||
So(err, ShouldBeNil)
|
||||
userBucket, err := userdb.CreateBucketIfNotExists([]byte(acCtx.Username))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = userBucket.Put([]byte(bolt.StarredReposKey), []byte("bad array"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return nil
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.ToggleStarRepo(ctx, "repo")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("ToggleBookmarkRepo, unmarshal error", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
"repo": true,
|
||||
},
|
||||
Username: "username",
|
||||
}
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
userdb, err := tx.CreateBucketIfNotExists([]byte(bolt.UserDataBucket))
|
||||
So(err, ShouldBeNil)
|
||||
userBucket, err := userdb.CreateBucketIfNotExists([]byte(acCtx.Username))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = userBucket.Put([]byte(bolt.BookmarkedReposKey), []byte("bad array"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return nil
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.ToggleBookmarkRepo(ctx, "repo")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("ToggleStarRepo, no repoMeta found", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
|
@ -832,6 +1006,73 @@ func TestWrapperErrors(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserData bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
_, err := boltdbWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("SetUserData bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := boltdbWrapper.SetUserData(ctx, repodb.UserData{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserGroups bad context errors", func() {
|
||||
_, err := boltdbWrapper.GetUserGroups(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
_, err = boltdbWrapper.GetUserGroups(ctx) //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("SetUserGroups bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := boltdbWrapper.SetUserGroups(ctx, []string{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("AddUserAPIKey bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := boltdbWrapper.AddUserAPIKey(ctx, "", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserAPIKey bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := boltdbWrapper.DeleteUserAPIKey(ctx, "")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("UpdateUserAPIKeyLastUsed bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := boltdbWrapper.UpdateUserAPIKeyLastUsed(ctx, "")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserData bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := boltdbWrapper.DeleteUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetStarredRepos bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
@ -840,60 +1081,6 @@ func TestWrapperErrors(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetStarredRepos user data unmarshal error", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
"repo": true,
|
||||
},
|
||||
Username: "username",
|
||||
}
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
userdb, err := tx.CreateBucketIfNotExists([]byte(bolt.UserDataBucket))
|
||||
So(err, ShouldBeNil)
|
||||
userBucket, err := userdb.CreateBucketIfNotExists([]byte(acCtx.Username))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = userBucket.Put([]byte(bolt.StarredReposKey), []byte("bad array"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return nil
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.GetStarredRepos(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetBookmarkedRepos user data unmarshal error", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
"repo": true,
|
||||
},
|
||||
Username: "username",
|
||||
}
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
userdb, err := tx.CreateBucketIfNotExists([]byte(bolt.UserDataBucket))
|
||||
So(err, ShouldBeNil)
|
||||
userBucket, err := userdb.CreateBucketIfNotExists([]byte(acCtx.Username))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = userBucket.Put([]byte(bolt.BookmarkedReposKey), []byte("bad array"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return nil
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.GetBookmarkedRepos(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetBookmarkedRepos bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
|
|
@ -31,6 +31,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
manifestDataTablename := "ManifestDataTable" + uuid.String()
|
||||
indexDataTablename := "IndexDataTable" + uuid.String()
|
||||
userDataTablename := "UserDataTable" + uuid.String()
|
||||
apiKeyTablename := "ApiKeyTable" + uuid.String()
|
||||
|
||||
versionTablename := "Version" + uuid.String()
|
||||
|
||||
|
@ -58,6 +59,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
IndexDataTablename: indexDataTablename,
|
||||
VersionTablename: versionTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
Patches: version.GetDynamoDBPatches(),
|
||||
Log: log.Logger{Logger: zerolog.New(os.Stdout)},
|
||||
}
|
||||
|
@ -74,6 +76,9 @@ func TestWrapperErrors(t *testing.T) {
|
|||
|
||||
err = dynamoWrapper.createVersionTable()
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = dynamoWrapper.createAPIKeyTable()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Delete table errors", t, func() {
|
||||
|
|
|
@ -43,6 +43,7 @@ func TestIterator(t *testing.T) {
|
|||
versionTablename := "Version" + uuid.String()
|
||||
indexDataTablename := "IndexDataTable" + uuid.String()
|
||||
userDataTablename := "UserDataTable" + uuid.String()
|
||||
apiKeyTablename := "ApiKeyTable" + uuid.String()
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
|
||||
|
@ -54,6 +55,7 @@ func TestIterator(t *testing.T) {
|
|||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
VersionTablename: versionTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
}
|
||||
client, err := dynamo.GetDynamoClient(params)
|
||||
|
@ -144,8 +146,8 @@ func TestWrapperErrors(t *testing.T) {
|
|||
versionTablename := "Version" + uuid.String()
|
||||
indexDataTablename := "IndexDataTable" + uuid.String()
|
||||
userDataTablename := "UserDataTable" + uuid.String()
|
||||
|
||||
ctx := context.Background()
|
||||
apiKeyTablename := "ApiKeyTable" + uuid.String()
|
||||
wrongTableName := "WRONG Tables"
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
|
||||
|
@ -157,6 +159,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
VersionTablename: versionTablename,
|
||||
}
|
||||
client, err := dynamo.GetDynamoClient(params) //nolint:contextcheck
|
||||
|
@ -168,6 +171,61 @@ func TestWrapperErrors(t *testing.T) {
|
|||
So(dynamoWrapper.ResetManifestDataTable(), ShouldBeNil) //nolint:contextcheck
|
||||
So(dynamoWrapper.ResetRepoMetaTable(), ShouldBeNil) //nolint:contextcheck
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
Convey("SetUserData", func() {
|
||||
hashKey := "id"
|
||||
apiKeys := make(map[string]repodb.APIKeyDetails)
|
||||
apiKeyDetails := repodb.APIKeyDetails{
|
||||
Label: "apiKey",
|
||||
Scopes: []string{"repo"},
|
||||
UUID: hashKey,
|
||||
}
|
||||
|
||||
apiKeys[hashKey] = apiKeyDetails
|
||||
|
||||
userProfileSrc := repodb.UserData{
|
||||
Groups: []string{"group1", "group2"},
|
||||
APIKeys: apiKeys,
|
||||
}
|
||||
|
||||
err := dynamoWrapper.SetUserData(ctx, userProfileSrc)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = dynamoWrapper.SetUserData(ctx, repodb.UserData{}) //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserData", func() {
|
||||
err := dynamoWrapper.DeleteUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = dynamoWrapper.DeleteUserData(ctx) //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("ToggleBookmarkRepo no access", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
|
@ -290,17 +348,17 @@ func TestWrapperErrors(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserMeta bad context", func() {
|
||||
Convey("GetUserData bad context", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
userData, err := dynamoWrapper.GetUserMeta(ctx)
|
||||
userData, err := dynamoWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(userData.BookmarkedRepos, ShouldBeEmpty)
|
||||
So(userData.StarredRepos, ShouldBeEmpty)
|
||||
})
|
||||
|
||||
Convey("GetUserMeta client error", func() {
|
||||
Convey("GetUserData client error", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
ReadGlobPatterns: map[string]bool{
|
||||
"repo": true,
|
||||
|
@ -312,7 +370,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
|
||||
dynamoWrapper.UserDataTablename = badTablename
|
||||
|
||||
_, err := dynamoWrapper.GetUserMeta(ctx)
|
||||
_, err := dynamoWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
|
@ -329,27 +387,155 @@ func TestWrapperErrors(t *testing.T) {
|
|||
err := setBadUserData(dynamoWrapper.Client, userDataTablename, acCtx.Username)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = dynamoWrapper.GetUserMeta(ctx)
|
||||
_, err = dynamoWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("SetUserMeta bad context", func() {
|
||||
Convey("SetUserData bad context", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := dynamoWrapper.SetUserMeta(ctx, repodb.UserData{})
|
||||
err := dynamoWrapper.SetUserData(ctx, repodb.UserData{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserData bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
_, err := dynamoWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("SetUserData bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := dynamoWrapper.SetUserData(ctx, repodb.UserData{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("AddUserAPIKey bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := dynamoWrapper.AddUserAPIKey(ctx, "", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserAPIKey bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := dynamoWrapper.DeleteUserAPIKey(ctx, "")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("UpdateUserAPIKeyLastUsed bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := dynamoWrapper.UpdateUserAPIKeyLastUsed(ctx, "")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserData bad context errors", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, "bad context")
|
||||
|
||||
err := dynamoWrapper.DeleteUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("DeleteUserAPIKey returns nil", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "email",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
apiKeyDetails := make(map[string]repodb.APIKeyDetails)
|
||||
apiKeyDetails["id"] = repodb.APIKeyDetails{
|
||||
UUID: "id",
|
||||
}
|
||||
err := dynamoWrapper.SetUserData(ctx, repodb.UserData{
|
||||
APIKeys: apiKeyDetails,
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dynamoWrapper.APIKeyTablename = wrongTableName
|
||||
err = dynamoWrapper.DeleteUserAPIKey(ctx, "id")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("AddUserAPIKey", func() {
|
||||
Convey("no userid found", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err = dynamoWrapper.AddUserAPIKey(ctx, "key", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "email",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err := dynamoWrapper.AddUserAPIKey(ctx, "key", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dynamoWrapper.APIKeyTablename = wrongTableName
|
||||
err = dynamoWrapper.AddUserAPIKey(ctx, "key", &repodb.APIKeyDetails{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserAPIKeyInfo", func() {
|
||||
dynamoWrapper.APIKeyTablename = wrongTableName
|
||||
_, err := dynamoWrapper.GetUserAPIKeyInfo("key")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetUserData", func() {
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
_, err := dynamoWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
acCtx = localCtx.AccessControlContext{
|
||||
Username: "email",
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
dynamoWrapper.UserDataTablename = wrongTableName
|
||||
_, err = dynamoWrapper.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("SetManifestData", func() {
|
||||
dynamoWrapper.ManifestDataTablename = "WRONG tables"
|
||||
dynamoWrapper.ManifestDataTablename = wrongTableName
|
||||
|
||||
err := dynamoWrapper.SetManifestData("dig", repodb.ManifestData{})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetManifestData", func() {
|
||||
dynamoWrapper.ManifestDataTablename = "WRONG table"
|
||||
dynamoWrapper.ManifestDataTablename = wrongTableName
|
||||
|
||||
_, err := dynamoWrapper.GetManifestData("dig")
|
||||
So(err, ShouldNotBeNil)
|
||||
|
@ -364,7 +550,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
})
|
||||
|
||||
Convey("GetIndexData", func() {
|
||||
dynamoWrapper.IndexDataTablename = "WRONG table"
|
||||
dynamoWrapper.IndexDataTablename = wrongTableName
|
||||
|
||||
_, err := dynamoWrapper.GetIndexData("dig")
|
||||
So(err, ShouldNotBeNil)
|
||||
|
@ -1091,6 +1277,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
VersionTablename: versionTablename,
|
||||
}
|
||||
client, err := dynamo.GetDynamoClient(params)
|
||||
|
@ -1106,6 +1293,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
ManifestDataTablename: "",
|
||||
IndexDataTablename: indexDataTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
VersionTablename: versionTablename,
|
||||
}
|
||||
client, err = dynamo.GetDynamoClient(params)
|
||||
|
@ -1121,6 +1309,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: "",
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
VersionTablename: versionTablename,
|
||||
}
|
||||
client, err = dynamo.GetDynamoClient(params)
|
||||
|
@ -1136,6 +1325,7 @@ func TestWrapperErrors(t *testing.T) {
|
|||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
VersionTablename: "",
|
||||
}
|
||||
client, err = dynamo.GetDynamoClient(params)
|
||||
|
@ -1150,8 +1340,41 @@ func TestWrapperErrors(t *testing.T) {
|
|||
RepoMetaTablename: repoMetaTablename,
|
||||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
UserDataTablename: "",
|
||||
VersionTablename: versionTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
}
|
||||
client, err = dynamo.GetDynamoClient(params)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = dynamoWrapper.NewDynamoDBWrapper(client, params, log)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
params = dynamo.DBDriverParameters{ //nolint:contextcheck
|
||||
Endpoint: endpoint,
|
||||
Region: region,
|
||||
RepoMetaTablename: repoMetaTablename,
|
||||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
VersionTablename: versionTablename,
|
||||
UserDataTablename: "",
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
}
|
||||
client, err = dynamo.GetDynamoClient(params)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = dynamoWrapper.NewDynamoDBWrapper(client, params, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
params = dynamo.DBDriverParameters{ //nolint:contextcheck
|
||||
Endpoint: endpoint,
|
||||
Region: region,
|
||||
RepoMetaTablename: repoMetaTablename,
|
||||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
VersionTablename: versionTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: "",
|
||||
}
|
||||
client, err = dynamo.GetDynamoClient(params)
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -1250,7 +1473,7 @@ func setBadUserData(client *dynamodb.Client, userDataTablename, userID string) e
|
|||
":UserData": userAttributeValue,
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"UserID": &types.AttributeValueMemberS{
|
||||
"Identity": &types.AttributeValueMemberS{
|
||||
Value: userID,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ var errRepodb = errors.New("repodb: error while constructing manifest meta")
|
|||
|
||||
type DBWrapper struct {
|
||||
Client *dynamodb.Client
|
||||
APIKeyTablename string
|
||||
RepoMetaTablename string
|
||||
IndexDataTablename string
|
||||
ManifestDataTablename string
|
||||
|
@ -47,6 +48,7 @@ func NewDynamoDBWrapper(client *dynamodb.Client, params dynamo.DBDriverParameter
|
|||
IndexDataTablename: params.IndexDataTablename,
|
||||
VersionTablename: params.VersionTablename,
|
||||
UserDataTablename: params.UserDataTablename,
|
||||
APIKeyTablename: params.APIKeyTablename,
|
||||
Patches: version.GetDynamoDBPatches(),
|
||||
Log: log,
|
||||
}
|
||||
|
@ -76,6 +78,11 @@ func NewDynamoDBWrapper(client *dynamodb.Client, params dynamo.DBDriverParameter
|
|||
return nil, err
|
||||
}
|
||||
|
||||
err = dynamoWrapper.createAPIKeyTable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Using the Config value, create the DynamoDB client
|
||||
return &dynamoWrapper, nil
|
||||
}
|
||||
|
@ -580,13 +587,13 @@ func (dwr *DBWrapper) GetUserRepoMeta(ctx context.Context, repo string) (repodb.
|
|||
return repodb.RepoMetadata{}, err
|
||||
}
|
||||
|
||||
userMeta, err := dwr.GetUserMeta(ctx)
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil {
|
||||
return repodb.RepoMetadata{}, err
|
||||
}
|
||||
|
||||
repoMeta.IsBookmarked = zcommon.Contains(userMeta.BookmarkedRepos, repo)
|
||||
repoMeta.IsStarred = zcommon.Contains(userMeta.StarredRepos, repo)
|
||||
repoMeta.IsBookmarked = zcommon.Contains(userData.BookmarkedRepos, repo)
|
||||
repoMeta.IsStarred = zcommon.Contains(userData.StarredRepos, repo)
|
||||
|
||||
return repoMeta, nil
|
||||
}
|
||||
|
@ -1802,7 +1809,7 @@ func (dwr *DBWrapper) ToggleBookmarkRepo(ctx context.Context, repo string) (
|
|||
return res, zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
userMeta, err := dwr.GetUserMeta(ctx)
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return repodb.NotChanged, nil
|
||||
|
@ -1811,16 +1818,16 @@ func (dwr *DBWrapper) ToggleBookmarkRepo(ctx context.Context, repo string) (
|
|||
return res, err
|
||||
}
|
||||
|
||||
if !zcommon.Contains(userMeta.BookmarkedRepos, repo) {
|
||||
userMeta.BookmarkedRepos = append(userMeta.BookmarkedRepos, repo)
|
||||
if !zcommon.Contains(userData.BookmarkedRepos, repo) {
|
||||
userData.BookmarkedRepos = append(userData.BookmarkedRepos, repo)
|
||||
res = repodb.Added
|
||||
} else {
|
||||
userMeta.BookmarkedRepos = zcommon.RemoveFrom(userMeta.BookmarkedRepos, repo)
|
||||
userData.BookmarkedRepos = zcommon.RemoveFrom(userData.BookmarkedRepos, repo)
|
||||
res = repodb.Removed
|
||||
}
|
||||
|
||||
if res != repodb.NotChanged {
|
||||
err = dwr.SetUserMeta(ctx, userMeta)
|
||||
err = dwr.SetUserData(ctx, userData)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -1833,9 +1840,9 @@ func (dwr *DBWrapper) ToggleBookmarkRepo(ctx context.Context, repo string) (
|
|||
}
|
||||
|
||||
func (dwr *DBWrapper) GetBookmarkedRepos(ctx context.Context) ([]string, error) {
|
||||
userMeta, err := dwr.GetUserMeta(ctx)
|
||||
userMeta, err := dwr.GetUserData(ctx)
|
||||
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) || errors.Is(err, zerr.ErrUserDataNotAllowed) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
|
@ -1863,7 +1870,7 @@ func (dwr *DBWrapper) ToggleStarRepo(ctx context.Context, repo string) (
|
|||
return res, zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
userData, err := dwr.GetUserMeta(ctx)
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return res, err
|
||||
}
|
||||
|
@ -1902,21 +1909,21 @@ func (dwr *DBWrapper) ToggleStarRepo(ctx context.Context, repo string) (
|
|||
_, err = dwr.Client.TransactWriteItems(ctx, &dynamodb.TransactWriteItemsInput{
|
||||
TransactItems: []types.TransactWriteItem{
|
||||
{
|
||||
// Update User Meta
|
||||
// Update User Profile
|
||||
Update: &types.Update{
|
||||
ExpressionAttributeNames: map[string]string{
|
||||
"#UM": "UserData",
|
||||
"#UP": "UserData",
|
||||
},
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":UserData": userAttributeValue,
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"UserID": &types.AttributeValueMemberS{
|
||||
"Identity": &types.AttributeValueMemberS{
|
||||
Value: userid,
|
||||
},
|
||||
},
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
UpdateExpression: aws.String("SET #UM = :UserData"),
|
||||
UpdateExpression: aws.String("SET #UP = :UserData"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1948,64 +1955,27 @@ func (dwr *DBWrapper) ToggleStarRepo(ctx context.Context, repo string) (
|
|||
}
|
||||
|
||||
func (dwr *DBWrapper) GetStarredRepos(ctx context.Context) ([]string, error) {
|
||||
userMeta, err := dwr.GetUserMeta(ctx)
|
||||
userMeta, err := dwr.GetUserData(ctx)
|
||||
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) || errors.Is(err, zerr.ErrUserDataNotAllowed) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return userMeta.StarredRepos, err
|
||||
}
|
||||
|
||||
func (dwr *DBWrapper) GetUserMeta(ctx context.Context) (repodb.UserData, error) {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return repodb.UserData{}, err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
|
||||
if userid == "" {
|
||||
// empty user is anonymous, it has no data
|
||||
return repodb.UserData{}, nil
|
||||
}
|
||||
|
||||
resp, err := dwr.Client.GetItem(ctx, &dynamodb.GetItemInput{
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
Key: map[string]types.AttributeValue{
|
||||
"UserID": &types.AttributeValueMemberS{Value: userid},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return repodb.UserData{}, err
|
||||
}
|
||||
|
||||
if resp.Item == nil {
|
||||
return repodb.UserData{}, zerr.ErrUserDataNotFound
|
||||
}
|
||||
|
||||
var userMeta repodb.UserData
|
||||
|
||||
err = attributevalue.Unmarshal(resp.Item["UserData"], &userMeta)
|
||||
if err != nil {
|
||||
return repodb.UserData{}, err
|
||||
}
|
||||
|
||||
return userMeta, nil
|
||||
}
|
||||
|
||||
func (dwr *DBWrapper) createUserDataTable() error {
|
||||
_, err := dwr.Client.CreateTable(context.Background(), &dynamodb.CreateTableInput{
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
AttributeDefinitions: []types.AttributeDefinition{
|
||||
{
|
||||
AttributeName: aws.String("UserID"),
|
||||
AttributeName: aws.String("Identity"),
|
||||
AttributeType: types.ScalarAttributeTypeS,
|
||||
},
|
||||
},
|
||||
KeySchema: []types.KeySchemaElement{
|
||||
{
|
||||
AttributeName: aws.String("UserID"),
|
||||
AttributeName: aws.String("Identity"),
|
||||
KeyType: types.KeyTypeHash,
|
||||
},
|
||||
},
|
||||
|
@ -2019,38 +1989,279 @@ func (dwr *DBWrapper) createUserDataTable() error {
|
|||
return dwr.waitTableToBeCreated(dwr.UserDataTablename)
|
||||
}
|
||||
|
||||
func (dwr *DBWrapper) SetUserMeta(ctx context.Context, userMeta repodb.UserData) error {
|
||||
func (dwr DBWrapper) createAPIKeyTable() error {
|
||||
_, err := dwr.Client.CreateTable(context.Background(), &dynamodb.CreateTableInput{
|
||||
TableName: aws.String(dwr.APIKeyTablename),
|
||||
AttributeDefinitions: []types.AttributeDefinition{
|
||||
{
|
||||
AttributeName: aws.String("HashedKey"),
|
||||
AttributeType: types.ScalarAttributeTypeS,
|
||||
},
|
||||
},
|
||||
KeySchema: []types.KeySchemaElement{
|
||||
{
|
||||
AttributeName: aws.String("HashedKey"),
|
||||
KeyType: types.KeyTypeHash,
|
||||
},
|
||||
},
|
||||
BillingMode: types.BillingModePayPerRequest,
|
||||
})
|
||||
|
||||
if err != nil && !strings.Contains(err.Error(), "Table already exists") {
|
||||
return err
|
||||
}
|
||||
|
||||
return dwr.waitTableToBeCreated(dwr.APIKeyTablename)
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) SetUserGroups(ctx context.Context, groups []string) error {
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
userData.Groups = append(userData.Groups, groups...)
|
||||
|
||||
return dwr.SetUserData(ctx, userData)
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) GetUserGroups(ctx context.Context) ([]string, error) {
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
|
||||
return userData.Groups, err
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error {
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiKeyDetails := userData.APIKeys[hashedKey]
|
||||
apiKeyDetails.LastUsed = time.Now()
|
||||
|
||||
userData.APIKeys[hashedKey] = apiKeyDetails
|
||||
|
||||
err = dwr.SetUserData(ctx, userData)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *repodb.APIKeyDetails) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
|
||||
if userid == "" {
|
||||
// empty user is anonymous, it has no data
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
userAttributeValue, err := attributevalue.Marshal(userMeta)
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
return fmt.Errorf("repoDB: error while getting userData for identity %s %w", userid, err)
|
||||
}
|
||||
|
||||
if userData.APIKeys == nil {
|
||||
userData.APIKeys = make(map[string]repodb.APIKeyDetails)
|
||||
}
|
||||
|
||||
userData.APIKeys[hashedKey] = *apiKeyDetails
|
||||
|
||||
userAttributeValue, err := attributevalue.Marshal(userData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dwr.Client.TransactWriteItems(ctx, &dynamodb.TransactWriteItemsInput{
|
||||
TransactItems: []types.TransactWriteItem{
|
||||
{
|
||||
// Update UserData
|
||||
Update: &types.Update{
|
||||
ExpressionAttributeNames: map[string]string{
|
||||
"#UP": "UserData",
|
||||
},
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":UserData": userAttributeValue,
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"Identity": &types.AttributeValueMemberS{
|
||||
Value: userid,
|
||||
},
|
||||
},
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
UpdateExpression: aws.String("SET #UP = :UserData"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Update APIKeyInfo
|
||||
Update: &types.Update{
|
||||
ExpressionAttributeNames: map[string]string{
|
||||
"#EM": "Identity",
|
||||
},
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":Identity": &types.AttributeValueMemberS{Value: userid},
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"HashedKey": &types.AttributeValueMemberS{
|
||||
Value: hashedKey,
|
||||
},
|
||||
},
|
||||
TableName: aws.String(dwr.APIKeyTablename),
|
||||
UpdateExpression: aws.String("SET #EM = :Identity"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) DeleteUserAPIKey(ctx context.Context, keyID string) error {
|
||||
userData, err := dwr.GetUserData(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("repoDB: error while getting userData %w", err)
|
||||
}
|
||||
|
||||
for hash, apiKeyDetails := range userData.APIKeys {
|
||||
if apiKeyDetails.UUID == keyID {
|
||||
delete(userData.APIKeys, hash)
|
||||
|
||||
_, err = dwr.Client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
|
||||
TableName: aws.String(dwr.APIKeyTablename),
|
||||
Key: map[string]types.AttributeValue{
|
||||
"HashedKey": &types.AttributeValueMemberS{Value: hash},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("repoDB: error while deleting userAPIKey entry for hash %s %w", hash, err)
|
||||
}
|
||||
|
||||
err := dwr.SetUserData(ctx, userData)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) GetUserAPIKeyInfo(hashedKey string) (string, error) {
|
||||
var userid string
|
||||
|
||||
resp, err := dwr.Client.GetItem(context.Background(), &dynamodb.GetItemInput{
|
||||
TableName: aws.String(dwr.APIKeyTablename),
|
||||
Key: map[string]types.AttributeValue{
|
||||
"HashedKey": &types.AttributeValueMemberS{Value: hashedKey},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Item == nil {
|
||||
return "", zerr.ErrUserAPIKeyNotFound
|
||||
}
|
||||
|
||||
err = attributevalue.Unmarshal(resp.Item["Identity"], &userid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userid, nil
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) GetUserData(ctx context.Context) (repodb.UserData, error) {
|
||||
var userData repodb.UserData
|
||||
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return userData, err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return userData, zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
resp, err := dwr.Client.GetItem(ctx, &dynamodb.GetItemInput{
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
Key: map[string]types.AttributeValue{
|
||||
"Identity": &types.AttributeValueMemberS{Value: userid},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return repodb.UserData{}, err
|
||||
}
|
||||
|
||||
if resp.Item == nil {
|
||||
return repodb.UserData{}, zerr.ErrUserDataNotFound
|
||||
}
|
||||
|
||||
err = attributevalue.Unmarshal(resp.Item["UserData"], &userData)
|
||||
if err != nil {
|
||||
return repodb.UserData{}, err
|
||||
}
|
||||
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) SetUserData(ctx context.Context, userData repodb.UserData) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
userAttributeValue, err := attributevalue.Marshal(userData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dwr.Client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
|
||||
ExpressionAttributeNames: map[string]string{
|
||||
"#UM": "UserData",
|
||||
"#UP": "UserData",
|
||||
},
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":UserData": userAttributeValue,
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"UserID": &types.AttributeValueMemberS{
|
||||
"Identity": &types.AttributeValueMemberS{
|
||||
Value: userid,
|
||||
},
|
||||
},
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
UpdateExpression: aws.String("SET #UM = :UserData"),
|
||||
UpdateExpression: aws.String("SET #UP = :UserData"),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (dwr DBWrapper) DeleteUserData(ctx context.Context) error {
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userid := localCtx.GetUsernameFromContext(acCtx)
|
||||
if userid == "" {
|
||||
// empty user is anonymous
|
||||
return zerr.ErrUserDataNotAllowed
|
||||
}
|
||||
|
||||
_, err = dwr.Client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
|
||||
TableName: aws.String(dwr.UserDataTablename),
|
||||
Key: map[string]types.AttributeValue{
|
||||
"Identity": &types.AttributeValueMemberS{Value: userid},
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
|
|
|
@ -24,6 +24,7 @@ type (
|
|||
)
|
||||
|
||||
type RepoDB interface { //nolint:interfacebloat
|
||||
UserDB
|
||||
// IncrementRepoStars adds 1 to the star count of an image
|
||||
IncrementRepoStars(repo string) error
|
||||
|
||||
|
@ -111,6 +112,10 @@ type RepoDB interface { //nolint:interfacebloat
|
|||
FilterTags(ctx context.Context, filter FilterFunc,
|
||||
requestedPage PageInput) ([]RepoMetadata, map[string]ManifestMetadata, map[string]IndexData, common.PageInfo, error)
|
||||
|
||||
PatchDB() error
|
||||
}
|
||||
|
||||
type UserDB interface { //nolint:interfacebloat
|
||||
// GetStarredRepos returns starred repos and takes current user in consideration
|
||||
GetStarredRepos(ctx context.Context) ([]string, error)
|
||||
|
||||
|
@ -123,7 +128,24 @@ type RepoDB interface { //nolint:interfacebloat
|
|||
// ToggleBookmarkRepo adds/removes bookmarks on repos
|
||||
ToggleBookmarkRepo(ctx context.Context, reponame string) (ToggleState, error)
|
||||
|
||||
PatchDB() error
|
||||
// UserDB profile/api key CRUD
|
||||
GetUserData(ctx context.Context) (UserData, error)
|
||||
|
||||
SetUserData(ctx context.Context, userData UserData) error
|
||||
|
||||
SetUserGroups(ctx context.Context, groups []string) error
|
||||
|
||||
GetUserGroups(ctx context.Context) ([]string, error)
|
||||
|
||||
DeleteUserData(ctx context.Context) error
|
||||
|
||||
GetUserAPIKeyInfo(hashedKey string) (identity string, err error)
|
||||
|
||||
AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *APIKeyDetails) error
|
||||
|
||||
UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error
|
||||
|
||||
DeleteUserAPIKey(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
type ManifestMetadata struct {
|
||||
|
@ -195,12 +217,6 @@ type SignatureMetadata struct {
|
|||
LayersInfo []LayerInfo
|
||||
}
|
||||
|
||||
type UserData struct {
|
||||
// data for each user.
|
||||
StarredRepos []string
|
||||
BookmarkedRepos []string
|
||||
}
|
||||
|
||||
type SortCriteria string
|
||||
|
||||
const (
|
||||
|
@ -235,3 +251,20 @@ type FilterData struct {
|
|||
IsStarred bool
|
||||
IsBookmarked bool
|
||||
}
|
||||
|
||||
type UserData struct {
|
||||
StarredRepos []string
|
||||
BookmarkedRepos []string
|
||||
Groups []string
|
||||
APIKeys map[string]APIKeyDetails
|
||||
}
|
||||
|
||||
type APIKeyDetails struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatorUA string `json:"creatorUa"`
|
||||
GeneratedBy string `json:"generatedBy"`
|
||||
LastUsed time.Time `json:"lastUsed"`
|
||||
Label string `json:"label"`
|
||||
Scopes []string `json:"scopes"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@ func TestDynamoDBWrapper(t *testing.T) {
|
|||
versionTablename := "Version" + uuid.String()
|
||||
indexDataTablename := "IndexDataTable" + uuid.String()
|
||||
userDataTablename := "UserDataTable" + uuid.String()
|
||||
apiKeyTablename := "ApiKeyTable" + uuid.String()
|
||||
|
||||
Convey("DynamoDB Wrapper", t, func() {
|
||||
dynamoDBDriverParams := dynamo.DBDriverParameters{
|
||||
|
@ -101,6 +102,7 @@ func TestDynamoDBWrapper(t *testing.T) {
|
|||
IndexDataTablename: indexDataTablename,
|
||||
VersionTablename: versionTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
Region: "us-east-2",
|
||||
}
|
||||
|
||||
|
@ -137,6 +139,118 @@ func RunRepoDBTests(t *testing.T, repoDB repodb.RepoDB, preparationFuncs ...func
|
|||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("Test CRUD operations on UserData and API keys", func() {
|
||||
hashKey1 := "id"
|
||||
hashKey2 := "key"
|
||||
apiKeys := make(map[string]repodb.APIKeyDetails)
|
||||
apiKeyDetails := repodb.APIKeyDetails{
|
||||
Label: "apiKey",
|
||||
Scopes: []string{"repo"},
|
||||
UUID: hashKey1,
|
||||
}
|
||||
|
||||
apiKeys[hashKey1] = apiKeyDetails
|
||||
|
||||
userProfileSrc := repodb.UserData{
|
||||
Groups: []string{"group1", "group2"},
|
||||
APIKeys: apiKeys,
|
||||
}
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
|
||||
err := repoDB.AddUserAPIKey(ctx, hashKey1, &apiKeyDetails)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = repoDB.SetUserData(ctx, userProfileSrc)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
userProfile, err := repoDB.GetUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(userProfile.Groups, ShouldResemble, userProfileSrc.Groups)
|
||||
So(userProfile.APIKeys, ShouldContainKey, hashKey1)
|
||||
So(userProfile.APIKeys[hashKey1].Label, ShouldEqual, apiKeyDetails.Label)
|
||||
So(userProfile.APIKeys[hashKey1].Scopes, ShouldResemble, apiKeyDetails.Scopes)
|
||||
|
||||
lastUsed := userProfile.APIKeys[hashKey1].LastUsed
|
||||
|
||||
err = repoDB.UpdateUserAPIKeyLastUsed(ctx, hashKey1)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
userProfile, err = repoDB.GetUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(userProfile.APIKeys[hashKey1].LastUsed, ShouldHappenAfter, lastUsed)
|
||||
|
||||
userGroups, err := repoDB.GetUserGroups(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(userGroups, ShouldResemble, userProfileSrc.Groups)
|
||||
|
||||
apiKeyDetails.UUID = hashKey2
|
||||
err = repoDB.AddUserAPIKey(ctx, hashKey2, &apiKeyDetails)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
userProfile, err = repoDB.GetUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(userProfile.Groups, ShouldResemble, userProfileSrc.Groups)
|
||||
So(userProfile.APIKeys, ShouldContainKey, hashKey2)
|
||||
So(userProfile.APIKeys[hashKey2].Label, ShouldEqual, apiKeyDetails.Label)
|
||||
So(userProfile.APIKeys[hashKey2].Scopes, ShouldResemble, apiKeyDetails.Scopes)
|
||||
|
||||
email, err := repoDB.GetUserAPIKeyInfo(hashKey2)
|
||||
So(err, ShouldBeNil)
|
||||
So(email, ShouldEqual, "test")
|
||||
|
||||
err = repoDB.DeleteUserAPIKey(ctx, hashKey1)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
userProfile, err = repoDB.GetUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(userProfile.APIKeys), ShouldEqual, 1)
|
||||
So(userProfile.APIKeys, ShouldNotContainKey, hashKey1)
|
||||
|
||||
err = repoDB.DeleteUserAPIKey(ctx, hashKey2)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
userProfile, err = repoDB.GetUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(userProfile.APIKeys), ShouldEqual, 0)
|
||||
So(userProfile.APIKeys, ShouldNotContainKey, hashKey2)
|
||||
|
||||
// delete non existent api key
|
||||
err = repoDB.DeleteUserAPIKey(ctx, hashKey2)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = repoDB.DeleteUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
email, err = repoDB.GetUserAPIKeyInfo(hashKey2)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(email, ShouldBeEmpty)
|
||||
|
||||
email, err = repoDB.GetUserAPIKeyInfo(hashKey1)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(email, ShouldBeEmpty)
|
||||
|
||||
_, err = repoDB.GetUserData(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
userGroups, err = repoDB.GetUserGroups(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(userGroups, ShouldBeEmpty)
|
||||
|
||||
err = repoDB.SetUserGroups(ctx, userProfileSrc.Groups)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
userGroups, err = repoDB.GetUserGroups(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
So(userGroups, ShouldResemble, userProfileSrc.Groups)
|
||||
})
|
||||
|
||||
Convey("Test SetManifestData and GetManifestData", func() {
|
||||
configBlob, manifestBlob, err := generateTestImage()
|
||||
So(err, ShouldBeNil)
|
||||
|
|
|
@ -95,6 +95,9 @@ func getDynamoParams(cacheDriverConfig map[string]interface{}, log log.Logger) d
|
|||
indexDataTablename, ok := toStringIfOk(cacheDriverConfig, "indexdatatablename", log)
|
||||
allParametersOk = allParametersOk && ok
|
||||
|
||||
apiKeyTablename, ok := toStringIfOk(cacheDriverConfig, "apikeytablename", log)
|
||||
allParametersOk = allParametersOk && ok
|
||||
|
||||
versionTablename, ok := toStringIfOk(cacheDriverConfig, "versiontablename", log)
|
||||
allParametersOk = allParametersOk && ok
|
||||
|
||||
|
@ -112,6 +115,7 @@ func getDynamoParams(cacheDriverConfig map[string]interface{}, log log.Logger) d
|
|||
ManifestDataTablename: manifestDataTablename,
|
||||
IndexDataTablename: indexDataTablename,
|
||||
UserDataTablename: userDataTablename,
|
||||
APIKeyTablename: apiKeyTablename,
|
||||
VersionTablename: versionTablename,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ func TestCreateDynamo(t *testing.T) {
|
|||
ManifestDataTablename: "ManifestDataTable",
|
||||
IndexDataTablename: "IndexDataTable",
|
||||
UserDataTablename: "UserDataTable",
|
||||
APIKeyTablename: "ApiKeyTable",
|
||||
VersionTablename: "Version",
|
||||
Region: "us-east-2",
|
||||
}
|
||||
|
|
|
@ -390,6 +390,7 @@ func TestParseStorageDynamoWrapper(t *testing.T) {
|
|||
ManifestDataTablename: "ManifestDataTable",
|
||||
IndexDataTablename: "IndexDataTable",
|
||||
UserDataTablename: "UserDataTable",
|
||||
APIKeyTablename: "ApiKeyTable",
|
||||
VersionTablename: "Version",
|
||||
}
|
||||
|
||||
|
|
|
@ -127,6 +127,7 @@ func TestVersioningDynamoDB(t *testing.T) {
|
|||
ManifestDataTablename: "ManifestDataTable",
|
||||
IndexDataTablename: "IndexDataTable",
|
||||
UserDataTablename: "UserDataTable",
|
||||
APIKeyTablename: "ApiKeyTable",
|
||||
VersionTablename: "Version",
|
||||
}
|
||||
|
||||
|
|
|
@ -92,3 +92,29 @@ func (acCtx *AccessControlContext) matchesRepo(globPatterns map[string]bool, rep
|
|||
|
||||
return allowed
|
||||
}
|
||||
|
||||
// request-local context key.
|
||||
var amwCtxKey = Key(1) //nolint: gochecknoglobals
|
||||
|
||||
// pointer needed for use in context.WithValue.
|
||||
func GetAuthnMiddlewareCtxKey() *Key {
|
||||
return &amwCtxKey
|
||||
}
|
||||
|
||||
type AuthnMiddlewareContext struct {
|
||||
AuthnType string
|
||||
}
|
||||
|
||||
func GetAuthnMiddlewareContext(ctx context.Context) (*AuthnMiddlewareContext, error) {
|
||||
authnMiddlewareCtxKey := GetAuthnMiddlewareCtxKey()
|
||||
if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil {
|
||||
amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext)
|
||||
if !ok {
|
||||
return nil, errors.ErrBadType
|
||||
}
|
||||
|
||||
return &amCtx, nil
|
||||
}
|
||||
|
||||
return nil, nil //nolint: nilnil
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -38,6 +39,7 @@ import (
|
|||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/umoci"
|
||||
"github.com/phayes/freeport"
|
||||
"github.com/project-zot/mockoidc"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
||||
|
@ -1967,3 +1969,55 @@ func GetIndexBlobWithManifests(manifestDigests []godigest.Digest) ([]byte, error
|
|||
|
||||
return json.Marshal(indexContent)
|
||||
}
|
||||
|
||||
func MockOIDCRun() (*mockoidc.MockOIDC, error) {
|
||||
// Create a fresh RSA Private Key for token signing
|
||||
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) //nolint: gomnd
|
||||
|
||||
// Create an unstarted MockOIDC server
|
||||
mockServer, _ := mockoidc.NewServer(rsaKey)
|
||||
|
||||
// Create the net.Listener, kernel will chose a valid port
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
bearerMiddleware := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, req *http.Request) {
|
||||
// stateVal := req.Form.Get("state")
|
||||
header := req.Header.Get("Authorization")
|
||||
parts := strings.SplitN(header, " ", 2) //nolint: gomnd
|
||||
if header != "" {
|
||||
if strings.ToLower(parts[0]) == "bearer" {
|
||||
req.Header.Set("Authorization", strings.Join([]string{"Bearer", parts[1]}, " "))
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(response, req)
|
||||
})
|
||||
}
|
||||
|
||||
err := mockServer.AddMiddleware(bearerMiddleware)
|
||||
if err != nil {
|
||||
return mockServer, err
|
||||
}
|
||||
// tlsConfig can be nil if you want HTTP
|
||||
return mockServer, mockServer.Start(listener, nil)
|
||||
}
|
||||
|
||||
func CustomRedirectPolicy(noOfRedirect int) resty.RedirectPolicy {
|
||||
return resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= noOfRedirect {
|
||||
return fmt.Errorf("stopped after %d redirects", noOfRedirect) //nolint: goerr113
|
||||
}
|
||||
|
||||
for key, val := range via[len(via)-1].Header {
|
||||
req.Header[key] = val
|
||||
}
|
||||
|
||||
respCookies := req.Response.Cookies()
|
||||
for _, cookie := range respCookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -95,6 +95,24 @@ type RepoDBMock struct {
|
|||
|
||||
ToggleBookmarkRepoFn func(ctx context.Context, repo string) (repodb.ToggleState, error)
|
||||
|
||||
GetUserDataFn func(ctx context.Context) (repodb.UserData, error)
|
||||
|
||||
SetUserDataFn func(ctx context.Context, userProfile repodb.UserData) error
|
||||
|
||||
SetUserGroupsFn func(ctx context.Context, groups []string) error
|
||||
|
||||
GetUserGroupsFn func(ctx context.Context) ([]string, error)
|
||||
|
||||
DeleteUserDataFn func(ctx context.Context) error
|
||||
|
||||
GetUserAPIKeyInfoFn func(hashedKey string) (string, error)
|
||||
|
||||
AddUserAPIKeyFn func(ctx context.Context, hashedKey string, apiKeyDetails *repodb.APIKeyDetails) error
|
||||
|
||||
UpdateUserAPIKeyLastUsedFn func(ctx context.Context, hashedKey string) error
|
||||
|
||||
DeleteUserAPIKeyFn func(ctx context.Context, id string) error
|
||||
|
||||
PatchDBFn func() error
|
||||
}
|
||||
|
||||
|
@ -414,3 +432,75 @@ func (sdm RepoDBMock) ToggleBookmarkRepo(ctx context.Context, repo string) (repo
|
|||
|
||||
return repodb.NotChanged, nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) GetUserData(ctx context.Context) (repodb.UserData, error) {
|
||||
if sdm.GetUserDataFn != nil {
|
||||
return sdm.GetUserDataFn(ctx)
|
||||
}
|
||||
|
||||
return repodb.UserData{}, nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) SetUserData(ctx context.Context, userProfile repodb.UserData) error {
|
||||
if sdm.SetUserDataFn != nil {
|
||||
return sdm.SetUserDataFn(ctx, userProfile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) SetUserGroups(ctx context.Context, groups []string) error {
|
||||
if sdm.SetUserGroupsFn != nil {
|
||||
return sdm.SetUserGroupsFn(ctx, groups)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) GetUserGroups(ctx context.Context) ([]string, error) {
|
||||
if sdm.GetUserGroupsFn != nil {
|
||||
return sdm.GetUserGroupsFn(ctx)
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) DeleteUserData(ctx context.Context) error {
|
||||
if sdm.DeleteUserDataFn != nil {
|
||||
return sdm.DeleteUserDataFn(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) GetUserAPIKeyInfo(hashedKey string) (string, error) {
|
||||
if sdm.GetUserAPIKeyInfoFn != nil {
|
||||
return sdm.GetUserAPIKeyInfoFn(hashedKey)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *repodb.APIKeyDetails) error {
|
||||
if sdm.AddUserAPIKeyFn != nil {
|
||||
return sdm.AddUserAPIKeyFn(ctx, hashedKey, apiKeyDetails)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error {
|
||||
if sdm.UpdateUserAPIKeyLastUsedFn != nil {
|
||||
return sdm.UpdateUserAPIKeyLastUsedFn(ctx, hashedKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdm RepoDBMock) DeleteUserAPIKey(ctx context.Context, id string) error {
|
||||
if sdm.DeleteUserAPIKeyFn != nil {
|
||||
return sdm.DeleteUserAPIKeyFn(ctx, id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -39,12 +39,34 @@ function setup() {
|
|||
"manifestDataTablename": "ManifestDataTable",
|
||||
"indexDataTablename": "IndexDataTable",
|
||||
"userDataTablename": "UserDataTable",
|
||||
"apiKeyTablename":"ApiKeyTable",
|
||||
"versionTablename": "Version"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"auth": {
|
||||
"openid": {
|
||||
"providers": {
|
||||
"dex": {
|
||||
"issuer": "http://127.0.0.1:5556/dex",
|
||||
"clientid": "zot-client",
|
||||
"clientsecret": "ZXhhbXBsZS1hcHAtc2VjcmV0",
|
||||
"scopes": ["openid", "email", "groups"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"failDelay": 5
|
||||
},
|
||||
"accessControl": {
|
||||
"repositories": {
|
||||
"**": {
|
||||
"anonymousPolicy": ["read", "create"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
|
@ -80,6 +102,17 @@ function teardown() {
|
|||
awslocal dynamodb --region "us-east-2" delete-table --table-name "BlobTable"
|
||||
}
|
||||
|
||||
dex_session () {
|
||||
STATE=$(curl -L -f -s http://localhost:8080/openid/auth/login?provider=dex | grep -m 1 -oP '(?<=state=)[^ ]*"' | cut -d \" -f1)
|
||||
echo $STATE >&3
|
||||
curl -L -f -s "http://127.0.0.1:5556/dex/auth/mock?client_id=zot-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fopenid%2Fauth%2Fcallback%2Fdex&response_type=code&scope=profile+email+groups+openid&state=$STATE"
|
||||
}
|
||||
|
||||
@test "check dex is working" {
|
||||
run dex_session
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "check for local disk writes" {
|
||||
run skopeo --insecure-policy copy --dest-tls-verify=false \
|
||||
docker://centos:centos8 docker://localhost:8080/centos:8
|
||||
|
|
28
test/dex/config-dev.yaml
Normal file
28
test/dex/config-dev.yaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
issuer: http://127.0.0.1:5556/dex
|
||||
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: dex.db
|
||||
|
||||
web:
|
||||
http: 127.0.0.1:5556
|
||||
|
||||
telemetry:
|
||||
http: 127.0.0.1:5558
|
||||
|
||||
grpc:
|
||||
addr: 127.0.0.1:5557
|
||||
|
||||
staticClients:
|
||||
- id: zot-client
|
||||
redirectURIs:
|
||||
- 'http://127.0.0.1:8080/openid/auth/callback/dex'
|
||||
name: 'zot'
|
||||
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
|
||||
|
||||
connectors:
|
||||
- type: mockCallback
|
||||
id: mock
|
||||
name: Example
|
||||
enablePasswordDB: true
|
Loading…
Reference in a new issue