0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

feat(apikey): added route to list user api keys (#1708)

adding api key expiration date

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
peusebiu 2023-08-29 19:38:38 +03:00 committed by GitHub
parent 28858f695f
commit 6926bddd3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1132 additions and 153 deletions

View file

@ -331,13 +331,15 @@ Create an API key for the current user using the REST API
```
POST /auth/apikey
Body: {"label": "git", "scopes": ["repo1", "repo2"]}'
Body: {"label": "git", "scopes": ["repo1", "repo2"], "expirationDate": "2023-08-28T17:10:05+03:00"}'
```
**Example cURL**
The time format of expirationDate is RFC1123Z.
**Example cURL without expiration date**
```bash
curl -u user:password -X POST http://localhost:8080/auth/apikey -d '{"label": "myLabel"}'
curl -u user:password -X POST http://localhost:8080/auth/apikey -d '{"label": "git", "scopes": ["repo1", "repo2"]}'
```
**Sample output**:
@ -345,16 +347,93 @@ curl -u user:password -X POST http://localhost:8080/auth/apikey -d '{"label": "m
```json
{
"createdAt": "2023-05-05T15:39:28.420926+03:00",
"expirationDate": "0001-01-01T00:00:00Z",
"isExpired": false,
"creatorUa": "curl/7.68.0",
"generatedBy": "manual",
"lastUsed": "2023-05-05T15:39:28.4209282+03:00",
"lastUsed": "0001-01-01T00:00:00Z",
"label": "git",
"scopes": null,
"scopes": [
"repo1",
"repo2"
],
"uuid": "46a45ce7-5d92-498a-a9cb-9654b1da3da1",
"apiKey": "zak_e77bcb9e9f634f1581756abbf9ecd269"
}
```
**Example cURL with expiration date**
```bash
curl -u user:password -X POST http://localhost:8080/auth/apikey -d '{"label": "myAPIKEY", "expirationDate": "2023-08-28T17:10:05+03:00"}'
```
**Sample output**:
```json
{
"createdAt":"2023-08-28T17:09:59.2603515+03:00",
"expirationDate":"2023-08-28T17:10:05+03:00",
"isExpired":false,
"creatorUa":"curl/7.68.0",
"generatedBy":"manual",
"lastUsed":"0001-01-01T00:00:00Z",
"label":"myAPIKEY",
"scopes":null,
"uuid":"c931e635-a80d-4b52-b035-6b57be5f6e74",
"apiKey":"zak_ac55a8693d6b4370a2003fa9e10b3682"
}
```
##### How to get list of API Keys
Get list of API keys for the current user using the REST API
**Usage**: GET /auth/apikey
**Produces**: application/json
**Example cURL**
```bash
curl -u user:password -X GET http://localhost:8080/auth/apikey
```
**Sample output**:
```json
{
"apiKeys": [
{
"createdAt": "2023-05-05T15:39:28.420926+03:00",
"expirationDate": "0001-01-01T00:00:00Z",
"isExpired": true,
"creatorUa": "curl/7.68.0",
"generatedBy": "manual",
"lastUsed": "0001-01-01T00:00:00Z",
"label": "git",
"scopes": [
"repo1",
"repo2"
],
"uuid": "46a45ce7-5d92-498a-a9cb-9654b1da3da1"
},
{
"createdAt": "2023-08-11T14:43:00.6459729+03:00",
"expirationDate": "2023-08-17T18:24:05+03:00",
"isExpired": false,
"creatorUa": "curl/7.68.0",
"generatedBy": "manual",
"lastUsed": "2023-08-11T14:43:47.5559998+03:00",
"label": "myAPIKEY",
"scopes": null,
"uuid": "294abf69-b62f-4e58-b214-dad2aec0bc52"
}
]
}
```
##### How to use API Keys
**Using API keys with cURL**

View file

@ -5,7 +5,14 @@
},
"http": {
"address": "127.0.0.1",
"port": "8080"
"port": "8080",
"auth": {
"apikey": true,
"htpasswd": {
"path": "/home/peusebiu/htpasswd"
},
"failDelay": 5
}
},
"log": {
"level": "debug"

View file

@ -203,7 +203,19 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
if storedIdentity == identity {
ctx := getReqContextWithAuthorization(identity, []string{}, request)
err := ctlr.MetaDB.UpdateUserAPIKeyLastUsed(ctx, hashedKey)
// check if api key expired
isExpired, err := ctlr.MetaDB.IsAPIKeyExpired(ctx, hashedKey)
if err != nil {
ctlr.Log.Err(err).Str("identity", identity).Msg("can not verify if api key expired")
return false, err
}
if isExpired {
return false, nil
}
err = ctlr.MetaDB.UpdateUserAPIKeyLastUsed(ctx, hashedKey)
if err != nil {
ctlr.Log.Err(err).Str("identity", identity).Msg("can not update user profile in DB")
@ -514,6 +526,8 @@ func (rh *RouteHandler) AuthURLHandler() http.HandlerFunc {
http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(http.StatusBadRequest)
})(w, r)
return
}
/* save cookie containing state to later verify it and

View file

@ -12,6 +12,7 @@ import (
"net/http/httptest"
"os"
"testing"
"time"
guuid "github.com/gofrs/uuid"
"github.com/project-zot/mockoidc"
@ -38,6 +39,12 @@ type (
}
)
type (
apiKeyListResponse struct {
APIKeys []mTypes.APIKeyDetails `json:"apiKeys"`
}
)
func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
defaultVal := true
@ -58,7 +65,7 @@ func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
resp, _ := resty.R().Options(baseURL + constants.APIKeyPath)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,DELETE,OPTIONS")
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,DELETE,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}
@ -135,7 +142,6 @@ func TestAPIKeys(t *testing.T) {
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").
@ -161,6 +167,24 @@ func TestAPIKeys(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// get API key list with basic auth
resp, err = resty.R().
SetBasicAuth("test", "test").
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var apiKeyListResponse apiKeyListResponse
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
// add another one
resp, err = resty.R().
SetBody(reqBody).
@ -179,9 +203,21 @@ func TestAPIKeys(t *testing.T) {
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// get API key list with api key auth
resp, err = resty.R().
SetBasicAuth("test", apiKeyResponse.APIKey).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 2)
})
Convey("API key retrieved with openID", func() {
Convey("API key retrieved with openID and with no expire", func() {
client := resty.New()
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
@ -197,7 +233,6 @@ func TestAPIKeys(t *testing.T) {
// call endpoint without session
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
@ -225,6 +260,25 @@ func TestAPIKeys(t *testing.T) {
email := user.Email
So(email, ShouldNotBeEmpty)
// get API key list
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var apiKeyListResponse apiKeyListResponse
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + "/v2/_catalog")
@ -290,7 +344,16 @@ func TestAPIKeys(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
})
Convey("Login with openid and create API key", func() {
Convey("API key retrieved with openID and with long expire", func() {
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
ExpirationDate: time.Now().Add(time.Hour).Local().Format(constants.APIKeyTimeFormat),
}
reqBody, err := json.Marshal(payload)
So(err, ShouldBeNil)
client := resty.New()
// mgmt should work both unauthenticated and authenticated
@ -324,6 +387,25 @@ func TestAPIKeys(t *testing.T) {
err = json.Unmarshal(resp.Body(), &apiKeyResponse)
So(err, ShouldBeNil)
// get API key list
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var apiKeyListResponse apiKeyListResponse
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
user := mockoidc.DefaultUser()
email := user.Email
So(email, ShouldNotBeEmpty)
@ -354,6 +436,18 @@ func TestAPIKeys(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// get API key list
resp, err = resty.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
// invalid api keys
resp, err = client.R().
SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
@ -433,6 +527,13 @@ func TestAPIKeys(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + "/v2/_catalog")
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).
@ -460,6 +561,14 @@ func TestAPIKeys(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// apiKey removed, should get 401
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().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Delete(baseURL + constants.APIKeyPath)
@ -474,6 +583,25 @@ func TestAPIKeys(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// get API key list
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 0)
resp, err = client.R().
SetBasicAuth("test", "test").
SetQueryParam("id", apiKeyResponse.UUID).
@ -490,6 +618,205 @@ func TestAPIKeys(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
})
Convey("API key retrieved with openID and with short expire", func() {
expirationDate := time.Now().Add(1 * time.Second).Local().Round(time.Second)
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
ExpirationDate: expirationDate.Format(constants.APIKeyTimeFormat),
}
reqBody, err := json.Marshal(payload)
So(err, ShouldBeNil)
client := resty.New()
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
// first login user
resp, err := client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("provider", "oidc").
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.APIKeyPath)
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)
// get API key list
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var apiKeyListResponse apiKeyListResponse
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
So(apiKeyListResponse.APIKeys[0].IsExpired, ShouldEqual, false)
So(apiKeyListResponse.APIKeys[0].ExpirationDate.Equal(expirationDate), ShouldBeTrue)
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sleep past expire time
time.Sleep(1500 * time.Millisecond)
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// again for coverage
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// get API key list with session authn
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
So(apiKeyListResponse.APIKeys[0].IsExpired, ShouldEqual, true)
So(apiKeyListResponse.APIKeys[0].ExpirationDate.Equal(expirationDate), ShouldBeTrue)
// delete expired api key
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("id", apiKeyResponse.UUID).
Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// get API key list with session authn
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
So(err, ShouldBeNil)
So(len(apiKeyListResponse.APIKeys), ShouldEqual, 0)
})
Convey("Create API key with expirationDate before actual date", func() {
expirationDate := time.Now().Add(-5 * time.Second).Local().Round(time.Second)
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
ExpirationDate: expirationDate.Format(constants.APIKeyTimeFormat),
}
reqBody, err := json.Marshal(payload)
So(err, ShouldBeNil)
client := resty.New()
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
// first login user
resp, err := client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("provider", "oidc").
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.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
Convey("Create API key with unparsable expirationDate", func() {
expirationDate := time.Now().Add(-5 * time.Second).Local().Round(time.Second)
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
ExpirationDate: expirationDate.Format(time.RFC1123Z),
}
reqBody, err := json.Marshal(payload)
So(err, ShouldBeNil)
client := resty.New()
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
// first login user
resp, err := client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("provider", "oidc").
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.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
Convey("Test error handling when API Key handler reads the request body", func() {
request, _ := http.NewRequestWithContext(context.TODO(),
http.MethodPost, "baseURL", errReader(0))

View file

@ -1,5 +1,7 @@
package constants
import "time"
const (
ArtifactSpecRoutePrefix = "/oras/artifacts/v1"
RoutePrefix = "/v2"
@ -20,4 +22,5 @@ const (
SessionClientHeaderValue = "zot-ui"
APIKeysPrefix = "zak_"
CallbackUIQueryParam = "callback_ui"
APIKeyTimeFormat = time.RFC3339
)

View file

@ -90,10 +90,11 @@ func (rh *RouteHandler) SetupRoutes() {
apiKeyRouter.Use(authHandler)
apiKeyRouter.Use(BaseAuthzHandler(rh.c))
apiKeyRouter.Use(zcommon.ACHeadersMiddleware(rh.c.Config,
http.MethodPost, http.MethodDelete, http.MethodOptions))
http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodOptions))
apiKeyRouter.Use(zcommon.CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin))
apiKeyRouter.Methods(http.MethodPost, http.MethodOptions).HandlerFunc(rh.CreateAPIKey)
apiKeyRouter.Methods(http.MethodGet).HandlerFunc(rh.GetAPIKeys)
apiKeyRouter.Methods(http.MethodDelete).HandlerFunc(rh.RevokeAPIKey)
}
@ -2029,8 +2030,49 @@ func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request *
}
type APIKeyPayload struct { //nolint:revive
Label string `json:"label"`
Scopes []string `json:"scopes"`
Label string `json:"label"`
Scopes []string `json:"scopes"`
ExpirationDate string `json:"expirationDate"`
}
// GetAPIKeys godoc
// @Summary Get list of API keys for the current user
// @Description Get list of all API keys for a logged in user
// @Accept json
// @Produce json
// @Success 200 {string} string "ok"
// @Failure 401 {string} string "unauthorized"
// @Failure 500 {string} string "internal server error"
// @Router /auth/apikey [get].
func (rh *RouteHandler) GetAPIKeys(resp http.ResponseWriter, req *http.Request) {
apiKeys, err := rh.c.MetaDB.GetUserAPIKeys(req.Context())
if err != nil {
rh.c.Log.Error().Err(err).Msg("error getting list of API keys for user")
resp.WriteHeader(http.StatusInternalServerError)
return
}
apiKeyResponse := struct {
APIKeys []mTypes.APIKeyDetails `json:"apiKeys"`
}{
APIKeys: apiKeys,
}
json := jsoniter.ConfigCompatibleWithStandardLibrary
data, err := json.Marshal(apiKeyResponse)
if err != nil {
rh.c.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.StatusOK)
_, _ = resp.Write(data)
}
// CreateAPIKey godoc
@ -2071,14 +2113,35 @@ func (rh *RouteHandler) CreateAPIKey(resp http.ResponseWriter, req *http.Request
hashedAPIKey := hashUUID(apiKey)
createdAt := time.Now()
// won't expire if no value provided
expirationDate := time.Time{}
if payload.ExpirationDate != "" {
expirationDate, err = time.ParseInLocation(constants.APIKeyTimeFormat, payload.ExpirationDate, time.Local)
if err != nil {
resp.WriteHeader(http.StatusBadRequest)
return
}
if createdAt.After(expirationDate) {
resp.WriteHeader(http.StatusBadRequest)
return
}
}
apiKeyDetails := &mTypes.APIKeyDetails{
CreatedAt: time.Now(),
LastUsed: time.Now(),
CreatorUA: req.UserAgent(),
GeneratedBy: "manual",
Label: payload.Label,
Scopes: payload.Scopes,
UUID: apiKeyID,
CreatedAt: createdAt,
ExpirationDate: expirationDate,
IsExpired: false,
CreatorUA: req.UserAgent(),
GeneratedBy: "manual",
Label: payload.Label,
Scopes: payload.Scopes,
UUID: apiKeyID,
}
err = rh.c.MetaDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)

View file

@ -1416,79 +1416,112 @@ func TestRoutes(t *testing.T) {
})
Convey("Test API keys", func() {
var invalid struct{}
Convey("CreateAPIKey invalid access control context", func() {
var invalid struct{}
ctx := context.TODO()
key := localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, invalid)
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()
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
response := httptest.NewRecorder()
rthdlr.CreateAPIKey(response, request)
rthdlr.CreateAPIKey(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
acCtx := localCtx.AccessControlContext{
Username: "test",
}
request, _ = http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
response = httptest.NewRecorder()
ctx = context.TODO()
key = localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, acCtx)
rthdlr.GetAPIKeys(response, request)
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
response = httptest.NewRecorder()
resp = response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
rthdlr.CreateAPIKey(response, request)
Convey("CreateAPIKey bad request body", func() {
acCtx := localCtx.AccessControlContext{
Username: "test",
}
resp = response.Result()
defer resp.Body.Close()
ctx := context.TODO()
key := localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, acCtx)
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
response := httptest.NewRecorder()
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
}
reqBody, err := json.Marshal(payload)
So(err, ShouldBeNil)
rthdlr.CreateAPIKey(response, request)
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(reqBody))
response = httptest.NewRecorder()
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
})
ctlr.MetaDB = mocks.MetaDBMock{
AddUserAPIKeyFn: func(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
return ErrUnexpectedError
},
}
rthdlr.CreateAPIKey(response, request)
Convey("CreateAPIKey error on AddUserAPIKey", func() {
acCtx := localCtx.AccessControlContext{
Username: "test",
}
resp = response.Result()
defer resp.Body.Close()
ctx := context.TODO()
key := localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, acCtx)
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
}
reqBody, err := json.Marshal(payload)
So(err, ShouldBeNil)
request, _ = http.NewRequestWithContext(ctx, http.MethodDelete, baseURL, bytes.NewReader([]byte{}))
response = httptest.NewRecorder()
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(reqBody))
response := httptest.NewRecorder()
q := request.URL.Query()
q.Add("id", "apikeyid")
request.URL.RawQuery = q.Encode()
ctlr.MetaDB = mocks.MetaDBMock{
AddUserAPIKeyFn: func(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
return ErrUnexpectedError
},
}
ctlr.MetaDB = mocks.MetaDBMock{
DeleteUserAPIKeyFn: func(ctx context.Context, id string) error {
return ErrUnexpectedError
},
}
rthdlr.RevokeAPIKey(response, request)
rthdlr.CreateAPIKey(response, request)
resp = response.Result()
defer resp.Body.Close()
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
Convey("Revoke error on DeleteUserAPIKeyFn", func() {
acCtx := localCtx.AccessControlContext{
Username: "test",
}
ctx := context.TODO()
key := localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, acCtx)
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()
ctlr.MetaDB = mocks.MetaDBMock{
DeleteUserAPIKeyFn: func(ctx context.Context, id string) error {
return ErrUnexpectedError
},
}
rthdlr.RevokeAPIKey(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
})
Convey("Helper functions", func() {

View file

@ -1696,6 +1696,96 @@ func (bdw *BoltDB) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey strin
return err
}
func (bdw *BoltDB) IsAPIKeyExpired(ctx context.Context, hashedKey string) (bool, error) {
acCtx, err := localCtx.GetAccessControlContext(ctx)
if err != nil {
return false, err
}
userid := localCtx.GetUsernameFromContext(acCtx)
if userid == "" {
// empty user is anonymous
return false, zerr.ErrUserDataNotAllowed
}
var isExpired bool
err = bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen
var userData mTypes.UserData
err := bdw.getUserData(userid, tx, &userData)
if err != nil {
return err
}
apiKeyDetails := userData.APIKeys[hashedKey]
if apiKeyDetails.IsExpired {
isExpired = true
return nil
}
// if expiresAt is not nil value
if !apiKeyDetails.ExpirationDate.Equal(time.Time{}) && time.Now().After(apiKeyDetails.ExpirationDate) {
isExpired = true
apiKeyDetails.IsExpired = true
}
userData.APIKeys[hashedKey] = apiKeyDetails
err = bdw.setUserData(userid, tx, userData)
return err
})
return isExpired, err
}
func (bdw *BoltDB) GetUserAPIKeys(ctx context.Context) ([]mTypes.APIKeyDetails, error) {
apiKeys := make([]mTypes.APIKeyDetails, 0)
acCtx, err := localCtx.GetAccessControlContext(ctx)
if err != nil {
return nil, err
}
userid := localCtx.GetUsernameFromContext(acCtx)
if userid == "" {
// empty user is anonymous
return nil, zerr.ErrUserDataNotAllowed
}
err = bdw.DB.Update(func(transaction *bbolt.Tx) error {
var userData mTypes.UserData
err = bdw.getUserData(userid, transaction, &userData)
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
return err
}
for hashedKey, apiKeyDetails := range userData.APIKeys {
// if expiresAt is not nil value
if !apiKeyDetails.ExpirationDate.Equal(time.Time{}) && time.Now().After(apiKeyDetails.ExpirationDate) {
apiKeyDetails.IsExpired = true
}
userData.APIKeys[hashedKey] = apiKeyDetails
err = bdw.setUserData(userid, transaction, userData)
if err != nil {
return err
}
apiKeys = append(apiKeys, apiKeyDetails)
}
return nil
})
return apiKeys, err
}
func (bdw *BoltDB) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
acCtx, err := localCtx.GetAccessControlContext(ctx)
if err != nil {

View file

@ -1781,6 +1781,34 @@ func (dwr DynamoDB) GetUserGroups(ctx context.Context) ([]string, error) {
return userData.Groups, err
}
func (dwr *DynamoDB) IsAPIKeyExpired(ctx context.Context, hashedKey string) (bool, error) {
userData, err := dwr.GetUserData(ctx)
if err != nil {
return false, err
}
var isExpired bool
apiKeyDetails := userData.APIKeys[hashedKey]
if apiKeyDetails.IsExpired {
isExpired = true
return isExpired, nil
}
// if expiresAt is not nil value
if !apiKeyDetails.ExpirationDate.Equal(time.Time{}) && time.Now().After(apiKeyDetails.ExpirationDate) {
isExpired = true
apiKeyDetails.IsExpired = true
}
userData.APIKeys[hashedKey] = apiKeyDetails
err = dwr.SetUserData(ctx, userData)
return isExpired, err
}
func (dwr DynamoDB) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error {
userData, err := dwr.GetUserData(ctx)
if err != nil {
@ -1797,6 +1825,44 @@ func (dwr DynamoDB) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey stri
return err
}
func (dwr DynamoDB) GetUserAPIKeys(ctx context.Context) ([]mTypes.APIKeyDetails, error) {
apiKeys := make([]mTypes.APIKeyDetails, 0)
acCtx, err := localCtx.GetAccessControlContext(ctx)
if err != nil {
return nil, err
}
userid := localCtx.GetUsernameFromContext(acCtx)
if userid == "" {
// empty user is anonymous
return nil, zerr.ErrUserDataNotAllowed
}
userData, err := dwr.GetUserData(ctx)
if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) {
return nil, fmt.Errorf("metaDB: error while getting userData for identity %s %w", userid, err)
}
for hashedKey, apiKeyDetails := range userData.APIKeys {
// if expiresAt is not nil value
if !apiKeyDetails.ExpirationDate.Equal(time.Time{}) && time.Now().After(apiKeyDetails.ExpirationDate) {
apiKeyDetails.IsExpired = true
}
userData.APIKeys[hashedKey] = apiKeyDetails
err = dwr.SetUserData(ctx, userData)
if err != nil {
return nil, err
}
apiKeys = append(apiKeys, apiKeyDetails)
}
return apiKeys, nil
}
func (dwr DynamoDB) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
acCtx, err := localCtx.GetAccessControlContext(ctx)
if err != nil {

View file

@ -143,10 +143,11 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func
Convey("Test CRUD operations on UserData and API keys", func() {
hashKey1 := "id"
hashKey2 := "key"
label1 := "apiKey1"
apiKeys := make(map[string]mTypes.APIKeyDetails)
apiKeyDetails := mTypes.APIKeyDetails{
Label: "apiKey",
Label: label1,
Scopes: []string{"repo"},
UUID: hashKey1,
}
@ -158,99 +159,281 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func
APIKeys: apiKeys,
}
authzCtxKey := localCtx.GetContextKey()
Convey("Test basic operations on API keys", func() {
hashKey2 := "key"
label2 := "apiKey2"
acCtx := localCtx.AccessControlContext{
Username: "test",
}
authzCtxKey := localCtx.GetContextKey()
acCtx := localCtx.AccessControlContext{
Username: "test",
}
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
err := metaDB.AddUserAPIKey(ctx, hashKey1, &apiKeyDetails)
So(err, ShouldBeNil)
err := metaDB.AddUserAPIKey(ctx, hashKey1, &apiKeyDetails)
So(err, ShouldBeNil)
isExpired, err := metaDB.IsAPIKeyExpired(ctx, hashKey1)
So(isExpired, ShouldBeFalse)
So(err, ShouldBeNil)
err = metaDB.SetUserData(ctx, userProfileSrc)
So(err, ShouldBeNil)
storedAPIKeys, err := metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0], ShouldResemble, apiKeyDetails)
userProfile, err := metaDB.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)
userProfile, err := metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
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 = metaDB.SetUserData(ctx, userProfileSrc)
So(err, ShouldBeNil)
err = metaDB.UpdateUserAPIKeyLastUsed(ctx, hashKey1)
So(err, ShouldBeNil)
userProfile, err = metaDB.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)
userProfile, err = metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
So(userProfile.APIKeys[hashKey1].LastUsed, ShouldHappenAfter, lastUsed)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0], ShouldResemble, apiKeyDetails)
userGroups, err := metaDB.GetUserGroups(ctx)
So(err, ShouldBeNil)
So(userGroups, ShouldResemble, userProfileSrc.Groups)
lastUsed := userProfile.APIKeys[hashKey1].LastUsed
apiKeyDetails.UUID = hashKey2
err = metaDB.AddUserAPIKey(ctx, hashKey2, &apiKeyDetails)
So(err, ShouldBeNil)
err = metaDB.UpdateUserAPIKeyLastUsed(ctx, hashKey1)
So(err, ShouldBeNil)
userProfile, err = metaDB.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)
userProfile, err = metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
So(userProfile.APIKeys[hashKey1].LastUsed, ShouldHappenAfter, lastUsed)
email, err := metaDB.GetUserAPIKeyInfo(hashKey2)
So(err, ShouldBeNil)
So(email, ShouldEqual, "test")
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0].LastUsed, ShouldHappenAfter, lastUsed)
err = metaDB.DeleteUserAPIKey(ctx, hashKey1)
So(err, ShouldBeNil)
userGroups, err := metaDB.GetUserGroups(ctx)
So(err, ShouldBeNil)
So(userGroups, ShouldResemble, userProfileSrc.Groups)
userProfile, err = metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
So(len(userProfile.APIKeys), ShouldEqual, 1)
So(userProfile.APIKeys, ShouldNotContainKey, hashKey1)
apiKeyDetails.UUID = hashKey2
apiKeyDetails.Label = label2
err = metaDB.AddUserAPIKey(ctx, hashKey2, &apiKeyDetails)
So(err, ShouldBeNil)
err = metaDB.DeleteUserAPIKey(ctx, hashKey2)
So(err, ShouldBeNil)
userProfile, err = metaDB.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)
userProfile, err = metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
So(len(userProfile.APIKeys), ShouldEqual, 0)
So(userProfile.APIKeys, ShouldNotContainKey, hashKey2)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 2)
So(storedAPIKeys[0].Scopes, ShouldResemble, apiKeyDetails.Scopes)
So(storedAPIKeys[1].Scopes, ShouldResemble, apiKeyDetails.Scopes)
scopes := []string{storedAPIKeys[0].Label, storedAPIKeys[1].Label}
// order is not preserved when getting api keys from db
So(scopes, ShouldContain, label1)
So(scopes, ShouldContain, label2)
// delete non existent api key
err = metaDB.DeleteUserAPIKey(ctx, hashKey2)
So(err, ShouldBeNil)
email, err := metaDB.GetUserAPIKeyInfo(hashKey2)
So(err, ShouldBeNil)
So(email, ShouldEqual, "test")
err = metaDB.DeleteUserData(ctx)
So(err, ShouldBeNil)
email, err = metaDB.GetUserAPIKeyInfo(hashKey1)
So(err, ShouldBeNil)
So(email, ShouldEqual, "test")
email, err = metaDB.GetUserAPIKeyInfo(hashKey2)
So(err, ShouldNotBeNil)
So(email, ShouldBeEmpty)
err = metaDB.DeleteUserAPIKey(ctx, hashKey1)
So(err, ShouldBeNil)
email, err = metaDB.GetUserAPIKeyInfo(hashKey1)
So(err, ShouldNotBeNil)
So(email, ShouldBeEmpty)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0].Label, ShouldEqual, label2)
_, err = metaDB.GetUserData(ctx)
So(err, ShouldNotBeNil)
userProfile, err = metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
So(len(userProfile.APIKeys), ShouldEqual, 1)
userGroups, err = metaDB.GetUserGroups(ctx)
So(err, ShouldNotBeNil)
So(userGroups, ShouldBeEmpty)
err = metaDB.DeleteUserAPIKey(ctx, hashKey2)
So(err, ShouldBeNil)
err = metaDB.SetUserGroups(ctx, userProfileSrc.Groups)
So(err, ShouldBeNil)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 0)
userGroups, err = metaDB.GetUserGroups(ctx)
So(err, ShouldBeNil)
So(userGroups, ShouldResemble, userProfileSrc.Groups)
userProfile, err = metaDB.GetUserData(ctx)
So(err, ShouldBeNil)
So(len(userProfile.APIKeys), ShouldEqual, 0)
So(userProfile.APIKeys, ShouldNotContainKey, hashKey2)
// delete non existent api key
err = metaDB.DeleteUserAPIKey(ctx, hashKey2)
So(err, ShouldBeNil)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 0)
err = metaDB.DeleteUserData(ctx)
So(err, ShouldBeNil)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 0)
email, err = metaDB.GetUserAPIKeyInfo(hashKey2)
So(err, ShouldNotBeNil)
So(email, ShouldBeEmpty)
email, err = metaDB.GetUserAPIKeyInfo(hashKey1)
So(err, ShouldNotBeNil)
So(email, ShouldBeEmpty)
_, err = metaDB.GetUserData(ctx)
So(err, ShouldNotBeNil)
userGroups, err = metaDB.GetUserGroups(ctx)
So(err, ShouldNotBeNil)
So(userGroups, ShouldBeEmpty)
err = metaDB.SetUserGroups(ctx, userProfileSrc.Groups)
So(err, ShouldBeNil)
userGroups, err = metaDB.GetUserGroups(ctx)
So(err, ShouldBeNil)
So(userGroups, ShouldResemble, userProfileSrc.Groups)
})
Convey("Test API keys operations with invalid access control context", func() {
var invalid struct{}
ctx := context.TODO()
key := localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, invalid)
_, err := metaDB.GetUserAPIKeys(ctx)
So(err, ShouldNotBeNil)
err = metaDB.AddUserAPIKey(ctx, hashKey1, &apiKeyDetails)
So(err, ShouldNotBeNil)
isExpired, err := metaDB.IsAPIKeyExpired(ctx, hashKey1)
So(isExpired, ShouldBeFalse)
So(err, ShouldNotBeNil)
err = metaDB.DeleteUserAPIKey(ctx, hashKey1)
So(err, ShouldNotBeNil)
_, err = metaDB.GetUserData(ctx)
So(err, ShouldNotBeNil)
_, err = metaDB.GetUserGroups(ctx)
So(err, ShouldNotBeNil)
_, err = metaDB.GetUserAPIKeyInfo(hashKey1)
So(err, ShouldNotBeNil)
err = metaDB.UpdateUserAPIKeyLastUsed(ctx, hashKey1)
So(err, ShouldNotBeNil)
err = metaDB.SetUserData(ctx, userProfileSrc)
So(err, ShouldNotBeNil)
})
Convey("Test API keys operations with empty userid", func() {
acCtx := localCtx.AccessControlContext{
Username: "",
}
ctx := context.TODO()
key := localCtx.GetContextKey()
ctx = context.WithValue(ctx, key, acCtx)
_, err := metaDB.GetUserAPIKeys(ctx)
So(err, ShouldNotBeNil)
isExpired, err := metaDB.IsAPIKeyExpired(ctx, hashKey1)
So(isExpired, ShouldBeFalse)
So(err, ShouldNotBeNil)
err = metaDB.AddUserAPIKey(ctx, hashKey1, &apiKeyDetails)
So(err, ShouldNotBeNil)
err = metaDB.DeleteUserAPIKey(ctx, hashKey1)
So(err, ShouldNotBeNil)
_, err = metaDB.GetUserData(ctx)
So(err, ShouldNotBeNil)
_, err = metaDB.GetUserGroups(ctx)
So(err, ShouldNotBeNil)
_, err = metaDB.GetUserAPIKeyInfo(hashKey1)
So(err, ShouldNotBeNil)
err = metaDB.UpdateUserAPIKeyLastUsed(ctx, hashKey1)
So(err, ShouldNotBeNil)
err = metaDB.SetUserData(ctx, userProfileSrc)
So(err, ShouldNotBeNil)
})
Convey("Test API keys with short expiration date", func() {
expirationDate := time.Now().Add(500 * time.Millisecond).Local().Round(time.Millisecond)
apiKeyDetails.ExpirationDate = expirationDate
authzCtxKey := localCtx.GetContextKey()
acCtx := localCtx.AccessControlContext{
Username: "test",
}
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
err := metaDB.AddUserAPIKey(ctx, hashKey1, &apiKeyDetails)
So(err, ShouldBeNil)
storedAPIKeys, err := metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0].ExpirationDate, ShouldResemble, expirationDate)
So(storedAPIKeys[0].Label, ShouldEqual, apiKeyDetails.Label)
So(storedAPIKeys[0].Scopes, ShouldResemble, apiKeyDetails.Scopes)
isExpired, err := metaDB.IsAPIKeyExpired(ctx, hashKey1)
So(isExpired, ShouldBeFalse)
So(err, ShouldBeNil)
time.Sleep(600 * time.Millisecond)
Convey("GetUserAPIKeys detects api key expired", func() {
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0].IsExpired, ShouldBeTrue)
isExpired, err = metaDB.IsAPIKeyExpired(ctx, hashKey1)
So(isExpired, ShouldBeTrue)
So(err, ShouldBeNil)
})
Convey("IsAPIKeyExpired detects api key expired", func() {
isExpired, err = metaDB.IsAPIKeyExpired(ctx, hashKey1)
So(isExpired, ShouldBeTrue)
So(err, ShouldBeNil)
storedAPIKeys, err = metaDB.GetUserAPIKeys(ctx)
So(err, ShouldBeNil)
So(len(storedAPIKeys), ShouldEqual, 1)
So(storedAPIKeys[0].IsExpired, ShouldBeTrue)
})
})
})
Convey("Test SetManifestData and GetManifestData", func() {

View file

@ -149,8 +149,12 @@ type UserDB interface { //nolint:interfacebloat
GetUserAPIKeyInfo(hashedKey string) (identity string, err error)
GetUserAPIKeys(ctx context.Context) ([]APIKeyDetails, error)
AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *APIKeyDetails) error
IsAPIKeyExpired(ctx context.Context, hashedKey string) (bool, error)
UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error
DeleteUserAPIKey(ctx context.Context, id string) error
@ -252,11 +256,13 @@ type FilterData struct {
}
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"`
CreatedAt time.Time `json:"createdAt"`
ExpirationDate time.Time `json:"expirationDate"`
IsExpired bool `json:"isExpired"`
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"`
}

View file

@ -95,6 +95,10 @@ type MetaDBMock struct {
GetUserAPIKeyInfoFn func(hashedKey string) (string, error)
IsAPIKeyExpiredFn func(ctx context.Context, hashedKey string) (bool, error)
GetUserAPIKeysFn func(ctx context.Context) ([]mTypes.APIKeyDetails, error)
AddUserAPIKeyFn func(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error
UpdateUserAPIKeyLastUsedFn func(ctx context.Context, hashedKey string) error
@ -427,6 +431,22 @@ func (sdm MetaDBMock) GetUserAPIKeyInfo(hashedKey string) (string, error) {
return "", nil
}
func (sdm MetaDBMock) IsAPIKeyExpired(ctx context.Context, hashedKey string) (bool, error) {
if sdm.IsAPIKeyExpiredFn != nil {
return sdm.IsAPIKeyExpiredFn(ctx, hashedKey)
}
return false, nil
}
func (sdm MetaDBMock) GetUserAPIKeys(ctx context.Context) ([]mTypes.APIKeyDetails, error) {
if sdm.GetUserAPIKeysFn != nil {
return sdm.GetUserAPIKeysFn(ctx)
}
return nil, nil
}
func (sdm MetaDBMock) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
if sdm.AddUserAPIKeyFn != nil {
return sdm.AddUserAPIKeyFn(ctx, hashedKey, apiKeyDetails)

View file

@ -21,6 +21,36 @@ const docTemplate = `{
"basePath": "{{.BasePath}}",
"paths": {
"/auth/apikey": {
"get": {
"description": "Get list of all API keys for a logged in user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Get list of API keys for the current user",
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"401": {
"description": "unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "string"
}
}
}
},
"post": {
"description": "Can create an api key for a logged in user, based on the provided label and scopes.",
"consumes": [
@ -1139,6 +1169,9 @@ const docTemplate = `{
"api.APIKeyPayload": {
"type": "object",
"properties": {
"expirationDate": {
"type": "string"
},
"label": {
"type": "string"
},

View file

@ -12,6 +12,36 @@
},
"paths": {
"/auth/apikey": {
"get": {
"description": "Get list of all API keys for a logged in user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Get list of API keys for the current user",
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"401": {
"description": "unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "string"
}
}
}
},
"post": {
"description": "Can create an api key for a logged in user, based on the provided label and scopes.",
"consumes": [
@ -1130,6 +1160,9 @@
"api.APIKeyPayload": {
"type": "object",
"properties": {
"expirationDate": {
"type": "string"
},
"label": {
"type": "string"
},

View file

@ -1,6 +1,8 @@
definitions:
api.APIKeyPayload:
properties:
expirationDate:
type: string
label:
type: string
scopes:
@ -270,6 +272,26 @@ paths:
schema:
type: string
summary: Revokes one current user API key
get:
consumes:
- application/json
description: Get list of all API keys for a logged in user
produces:
- application/json
responses:
"200":
description: ok
schema:
type: string
"401":
description: unauthorized
schema:
type: string
"500":
description: internal server error
schema:
type: string
summary: Get list of API keys for the current user
post:
consumes:
- application/json