Access control: Pass access control metadata for api keys (#48445)
* Move ApiKeyDTO to dtos package * Add access control filter to api keys * pass user in GetApiKeysQuery * Add api key metadata to DTO * Remove scope all requirement from get api keys endpoint * Handle api key access control metadata in frondend
This commit is contained in:
+1
-1
@@ -278,7 +278,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// auth api keys
|
||||
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
|
||||
apikeyIDScope := ac.Scope("apikeys", "id", ac.Parameter(":id"))
|
||||
keysRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead, ac.ScopeAPIKeysAll)), routing.Wrap(hs.GetAPIKeys))
|
||||
keysRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead)), routing.Wrap(hs.GetAPIKeys))
|
||||
keysRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyCreate)), quota("api_key"), routing.Wrap(hs.AddAPIKey))
|
||||
keysRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyDelete, apikeyIDScope)), routing.Wrap(hs.DeleteAPIKey))
|
||||
})
|
||||
|
||||
+12
-3
@@ -15,20 +15,22 @@ import (
|
||||
|
||||
// GetAPIKeys returns a list of API keys
|
||||
func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response {
|
||||
query := models.GetApiKeysQuery{OrgId: c.OrgId, IncludeExpired: c.QueryBool("includeExpired")}
|
||||
query := models.GetApiKeysQuery{OrgId: c.OrgId, User: c.SignedInUser, IncludeExpired: c.QueryBool("includeExpired")}
|
||||
|
||||
if err := hs.SQLStore.GetAPIKeys(c.Req.Context(), &query); err != nil {
|
||||
return response.Error(500, "Failed to list api keys", err)
|
||||
}
|
||||
|
||||
result := make([]*models.ApiKeyDTO, len(query.Result))
|
||||
ids := map[string]bool{}
|
||||
result := make([]*dtos.ApiKeyDTO, len(query.Result))
|
||||
for i, t := range query.Result {
|
||||
ids[strconv.FormatInt(t.Id, 10)] = true
|
||||
var expiration *time.Time = nil
|
||||
if t.Expires != nil {
|
||||
v := time.Unix(*t.Expires, 0)
|
||||
expiration = &v
|
||||
}
|
||||
result[i] = &models.ApiKeyDTO{
|
||||
result[i] = &dtos.ApiKeyDTO{
|
||||
Id: t.Id,
|
||||
Name: t.Name,
|
||||
Role: t.Role,
|
||||
@@ -36,6 +38,13 @@ func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response {
|
||||
}
|
||||
}
|
||||
|
||||
metadata := hs.getMultiAccessControlMetadata(c, c.OrgId, "apikeys:id", ids)
|
||||
if len(metadata) > 0 {
|
||||
for _, key := range result {
|
||||
key.AccessControl = metadata[strconv.FormatInt(key.Id, 10)]
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ type DeleteAPIkeyParams struct {
|
||||
type GetAPIkeyResponse struct {
|
||||
// The response message
|
||||
// in: body
|
||||
Body []*models.ApiKeyDTO `json:"body"`
|
||||
Body []*dtos.ApiKeyDTO `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:response postAPIkeyResponse
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
type NewApiKeyResult struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type ApiKeyDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role models.RoleType `json:"role"`
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||
}
|
||||
|
||||
+1
-10
@@ -47,6 +47,7 @@ type DeleteApiKeyCommand struct {
|
||||
type GetApiKeysQuery struct {
|
||||
OrgId int64
|
||||
IncludeExpired bool
|
||||
User *SignedInUser
|
||||
Result []*ApiKey
|
||||
}
|
||||
|
||||
@@ -60,13 +61,3 @@ type GetApiKeyByIdQuery struct {
|
||||
ApiKeyId int64
|
||||
Result *ApiKey
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// DTO & Projections
|
||||
|
||||
type ApiKeyDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role RoleType `json:"role"`
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
var sqlIDAcceptList = map[string]struct{}{
|
||||
"id": {},
|
||||
"org_user.user_id": {},
|
||||
"role.id": {},
|
||||
"t.id": {},
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// GetAPIKeys queries the database based
|
||||
@@ -27,6 +29,14 @@ func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuer
|
||||
|
||||
sess = sess.Where("service_account_id IS NULL")
|
||||
|
||||
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
filter, err := accesscontrol.Filter(query.User, "id", "apikeys:id:", accesscontrol.ActionAPIKeyRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.And(filter.Where, filter.Args...)
|
||||
}
|
||||
|
||||
query.Result = make([]*models.ApiKey, 0)
|
||||
return sess.Find(&query.Result)
|
||||
})
|
||||
|
||||
@@ -5,10 +5,14 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -149,3 +153,60 @@ func TestApiKeyErrors(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type getApiKeysTestCase struct {
|
||||
desc string
|
||||
user *models.SignedInUser
|
||||
expectedNumKeys int
|
||||
}
|
||||
|
||||
func TestSQLStore_GetAPIKeys(t *testing.T) {
|
||||
tests := []getApiKeysTestCase{
|
||||
{
|
||||
desc: "expect all keys for wildcard scope",
|
||||
user: &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {"apikeys:*"}},
|
||||
}},
|
||||
expectedNumKeys: 10,
|
||||
},
|
||||
{
|
||||
desc: "expect only api keys that user have scopes for",
|
||||
user: &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {"apikeys:id:1", "apikeys:id:3"}},
|
||||
}},
|
||||
expectedNumKeys: 2,
|
||||
},
|
||||
{
|
||||
desc: "expect no keys when user have no scopes",
|
||||
user: &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {}},
|
||||
}},
|
||||
expectedNumKeys: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
store := InitTestDB(t, InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagAccesscontrol}})
|
||||
seedApiKeys(t, store, 10)
|
||||
|
||||
query := &models.GetApiKeysQuery{OrgId: 1, User: tt.user}
|
||||
err := store.GetAPIKeys(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, query.Result, tt.expectedNumKeys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func seedApiKeys(t *testing.T, store *SQLStore, num int) {
|
||||
t.Helper()
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
err := store.AddAPIKey(context.Background(), &models.AddApiKeyCommand{
|
||||
Name: fmt.Sprintf("key:%d", i),
|
||||
Key: fmt.Sprintf("key:%d", i),
|
||||
OrgId: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user