mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
feat: App Keys API v2 (#10140)
# Which Problems Are Solved This PR *partially* addresses #9450 . Specifically, it implements the resource based API for app keys. This PR, together with https://github.com/zitadel/zitadel/pull/10077 completes #9450 . # How the Problems Are Solved - Implementation of the following endpoints: `CreateApplicationKey`, `DeleteApplicationKey`, `GetApplicationKey`, `ListApplicationKeys` - `ListApplicationKeys` can filter by project, app or organization ID. Sorting is also possible according to some criteria. - All endpoints use permissions V2 # TODO - [x] Deprecate old endpoints # Additional Context Closes #9450
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
@@ -58,3 +60,39 @@ func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) app.APIAuthMetho
|
||||
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
|
||||
}
|
||||
}
|
||||
|
||||
func GetApplicationKeyQueriesRequestToDomain(orgID, projectID, appID string) ([]query.SearchQuery, error) {
|
||||
var searchQueries []query.SearchQuery
|
||||
|
||||
orgID, projectID, appID = strings.TrimSpace(orgID), strings.TrimSpace(projectID), strings.TrimSpace(appID)
|
||||
|
||||
if orgID != "" {
|
||||
resourceOwner, err := query.NewAuthNKeyResourceOwnerQuery(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchQueries = append(searchQueries, resourceOwner)
|
||||
}
|
||||
|
||||
if projectID != "" {
|
||||
aggregateID, err := query.NewAuthNKeyAggregateIDQuery(projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchQueries = append(searchQueries, aggregateID)
|
||||
}
|
||||
|
||||
if appID != "" {
|
||||
objectID, err := query.NewAuthNKeyObjectIDQuery(appID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchQueries = append(searchQueries, objectID)
|
||||
}
|
||||
|
||||
return searchQueries, nil
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@@ -147,3 +148,71 @@ func Test_apiAuthMethodTypeToPb(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestGetApplicationKeyQueriesRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputOrgID string
|
||||
inputProjectID string
|
||||
inputAppID string
|
||||
|
||||
expectedQueriesLength int
|
||||
}{
|
||||
{
|
||||
testName: "all IDs provided",
|
||||
inputOrgID: "org-1",
|
||||
inputProjectID: "proj-1",
|
||||
inputAppID: "app-1",
|
||||
expectedQueriesLength: 3,
|
||||
},
|
||||
{
|
||||
testName: "only org ID",
|
||||
inputOrgID: "org-1",
|
||||
inputProjectID: " ",
|
||||
inputAppID: "",
|
||||
expectedQueriesLength: 1,
|
||||
},
|
||||
{
|
||||
testName: "only project ID",
|
||||
inputOrgID: "",
|
||||
inputProjectID: "proj-1",
|
||||
inputAppID: " ",
|
||||
expectedQueriesLength: 1,
|
||||
},
|
||||
{
|
||||
testName: "only app ID",
|
||||
inputOrgID: " ",
|
||||
inputProjectID: "",
|
||||
inputAppID: "app-1",
|
||||
expectedQueriesLength: 1,
|
||||
},
|
||||
{
|
||||
testName: "empty IDs",
|
||||
inputOrgID: " ",
|
||||
inputProjectID: " ",
|
||||
inputAppID: " ",
|
||||
expectedQueriesLength: 0,
|
||||
},
|
||||
{
|
||||
testName: "with spaces",
|
||||
inputOrgID: " org-1 ",
|
||||
inputProjectID: " proj-1 ",
|
||||
inputAppID: " app-1 ",
|
||||
expectedQueriesLength: 3,
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
got, err := GetApplicationKeyQueriesRequestToDomain(tc.inputOrgID, tc.inputProjectID, tc.inputAppID)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, got, tc.expectedQueriesLength)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package convert
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
@@ -163,3 +165,98 @@ func appQueryToModel(appQuery *app.ApplicationSearchFilter) (query.SearchQuery,
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func CreateAPIClientKeyRequestToDomain(key *app.CreateApplicationKeyRequest) *domain.ApplicationKey {
|
||||
return &domain.ApplicationKey{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: strings.TrimSpace(key.GetProjectId()),
|
||||
},
|
||||
ExpirationDate: key.GetExpirationDate().AsTime(),
|
||||
Type: domain.AuthNKeyTypeJSON,
|
||||
ApplicationID: strings.TrimSpace(key.GetAppId()),
|
||||
}
|
||||
}
|
||||
|
||||
func ListApplicationKeysRequestToDomain(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationKeysRequest) (*query.AuthNKeySearchQueries, error) {
|
||||
var queries []query.SearchQuery
|
||||
|
||||
switch req.GetResourceId().(type) {
|
||||
case *app.ListApplicationKeysRequest_ApplicationId:
|
||||
object, err := query.NewAuthNKeyObjectIDQuery(strings.TrimSpace(req.GetApplicationId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, object)
|
||||
case *app.ListApplicationKeysRequest_OrganizationId:
|
||||
resourceOwner, err := query.NewAuthNKeyResourceOwnerQuery(strings.TrimSpace(req.GetOrganizationId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, resourceOwner)
|
||||
case *app.ListApplicationKeysRequest_ProjectId:
|
||||
aggregate, err := query.NewAuthNKeyAggregateIDQuery(strings.TrimSpace(req.GetProjectId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, aggregate)
|
||||
case nil:
|
||||
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-t3ENme", "unexpected resource id")
|
||||
}
|
||||
|
||||
offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &query.AuthNKeySearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
SortingColumn: appKeysSortingToColumn(req.GetSortingColumn()),
|
||||
},
|
||||
|
||||
Queries: queries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appKeysSortingToColumn(sortingCriteria app.ApplicationKeysSorting) query.Column {
|
||||
switch sortingCriteria {
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_PROJECT_ID:
|
||||
return query.AuthNKeyColumnAggregateID
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE:
|
||||
return query.AuthNKeyColumnCreationDate
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION:
|
||||
return query.AuthNKeyColumnExpiration
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_ORGANIZATION_ID:
|
||||
return query.AuthNKeyColumnResourceOwner
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_TYPE:
|
||||
return query.AuthNKeyColumnType
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_APPLICATION_ID:
|
||||
return query.AuthNKeyColumnObjectID
|
||||
case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_ID:
|
||||
fallthrough
|
||||
default:
|
||||
return query.AuthNKeyColumnID
|
||||
}
|
||||
}
|
||||
|
||||
func ApplicationKeysToPb(keys []*query.AuthNKey) []*app.ApplicationKey {
|
||||
pbAppKeys := make([]*app.ApplicationKey, len(keys))
|
||||
|
||||
for i, k := range keys {
|
||||
pbKey := &app.ApplicationKey{
|
||||
Id: k.ID,
|
||||
ApplicationId: k.ApplicationID,
|
||||
ProjectId: k.AggregateID,
|
||||
CreationDate: timestamppb.New(k.CreationDate),
|
||||
OrganizationId: k.ResourceOwner,
|
||||
ExpirationDate: timestamppb.New(k.Expiration),
|
||||
}
|
||||
pbAppKeys[i] = pbKey
|
||||
}
|
||||
|
||||
return pbAppKeys
|
||||
}
|
||||
|
@@ -518,3 +518,187 @@ func TestAppQueryToModel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListApplicationKeysRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
resourceOwnerQuery, err := query.NewAuthNKeyResourceOwnerQuery("org1")
|
||||
require.NoError(t, err)
|
||||
|
||||
projectIDQuery, err := query.NewAuthNKeyAggregateIDQuery("project1")
|
||||
require.NoError(t, err)
|
||||
|
||||
appIDQuery, err := query.NewAuthNKeyObjectIDQuery("app1")
|
||||
require.NoError(t, err)
|
||||
|
||||
sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
req *app.ListApplicationKeysRequest
|
||||
|
||||
expectedResult *query.AuthNKeySearchQueries
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "invalid pagination limit",
|
||||
req: &app.ListApplicationKeysRequest{
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)},
|
||||
},
|
||||
expectedResult: nil,
|
||||
expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
|
||||
},
|
||||
{
|
||||
name: "empty request",
|
||||
req: &app.ListApplicationKeysRequest{
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedResult: &query.AuthNKeySearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Asc: true,
|
||||
SortingColumn: query.AuthNKeyColumnID,
|
||||
},
|
||||
Queries: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only organization id",
|
||||
req: &app.ListApplicationKeysRequest{
|
||||
ResourceId: &app.ListApplicationKeysRequest_OrganizationId{OrganizationId: "org1"},
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedResult: &query.AuthNKeySearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Asc: true,
|
||||
SortingColumn: query.AuthNKeyColumnID,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
resourceOwnerQuery,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only project id",
|
||||
req: &app.ListApplicationKeysRequest{
|
||||
ResourceId: &app.ListApplicationKeysRequest_ProjectId{ProjectId: "project1"},
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedResult: &query.AuthNKeySearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Asc: true,
|
||||
SortingColumn: query.AuthNKeyColumnID,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
projectIDQuery,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only application id",
|
||||
req: &app.ListApplicationKeysRequest{
|
||||
ResourceId: &app.ListApplicationKeysRequest_ApplicationId{ApplicationId: "app1"},
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedResult: &query.AuthNKeySearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Asc: true,
|
||||
SortingColumn: query.AuthNKeyColumnID,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
appIDQuery,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result, err := ListApplicationKeysRequestToDomain(sysDefaults, tc.req)
|
||||
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationKeysToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
input []*query.AuthNKey
|
||||
expected []*app.ApplicationKey
|
||||
}{
|
||||
{
|
||||
name: "multiple keys",
|
||||
input: []*query.AuthNKey{
|
||||
{
|
||||
ID: "key1",
|
||||
AggregateID: "project1",
|
||||
ApplicationID: "app1",
|
||||
CreationDate: now,
|
||||
ResourceOwner: "org1",
|
||||
Expiration: now.Add(24 * time.Hour),
|
||||
Type: domain.AuthNKeyTypeJSON,
|
||||
},
|
||||
{
|
||||
ID: "key2",
|
||||
AggregateID: "project2",
|
||||
ApplicationID: "app1",
|
||||
CreationDate: now.Add(-time.Hour),
|
||||
ResourceOwner: "org2",
|
||||
Expiration: now.Add(48 * time.Hour),
|
||||
Type: domain.AuthNKeyTypeNONE,
|
||||
},
|
||||
},
|
||||
expected: []*app.ApplicationKey{
|
||||
{
|
||||
Id: "key1",
|
||||
ApplicationId: "app1",
|
||||
ProjectId: "project1",
|
||||
CreationDate: timestamppb.New(now),
|
||||
OrganizationId: "org1",
|
||||
ExpirationDate: timestamppb.New(now.Add(24 * time.Hour)),
|
||||
},
|
||||
{
|
||||
Id: "key2",
|
||||
ApplicationId: "app1",
|
||||
ProjectId: "project2",
|
||||
CreationDate: timestamppb.New(now.Add(-time.Hour)),
|
||||
OrganizationId: "org2",
|
||||
ExpirationDate: timestamppb.New(now.Add(48 * time.Hour)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty slice",
|
||||
input: []*query.AuthNKey{},
|
||||
expected: []*app.ApplicationKey{},
|
||||
},
|
||||
{
|
||||
name: "nil input",
|
||||
input: nil,
|
||||
expected: []*app.ApplicationKey{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := ApplicationKeysToPb(tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user