mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 04:48:26 +08: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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user