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:
Marco A.
2025-07-02 09:34:19 +02:00
committed by GitHub
parent 64a03fba28
commit fce9e770ac
19 changed files with 1350 additions and 69 deletions

View File

@@ -0,0 +1,47 @@
package app
import (
"context"
"strings"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func (s *Server) CreateApplicationKey(ctx context.Context, req *app.CreateApplicationKeyRequest) (*app.CreateApplicationKeyResponse, error) {
domainReq := convert.CreateAPIClientKeyRequestToDomain(req)
appKey, err := s.command.AddApplicationKey(ctx, domainReq, "")
if err != nil {
return nil, err
}
keyDetails, err := appKey.Detail()
if err != nil {
return nil, err
}
return &app.CreateApplicationKeyResponse{
Id: appKey.KeyID,
CreationDate: timestamppb.New(appKey.ChangeDate),
KeyDetails: keyDetails,
}, nil
}
func (s *Server) DeleteApplicationKey(ctx context.Context, req *app.DeleteApplicationKeyRequest) (*app.DeleteApplicationKeyResponse, error) {
deletionDetails, err := s.command.RemoveApplicationKey(ctx,
strings.TrimSpace(req.GetProjectId()),
strings.TrimSpace(req.GetApplicationId()),
strings.TrimSpace(req.GetId()),
strings.TrimSpace(req.GetOrganizationId()),
)
if err != nil {
return nil, err
}
return &app.DeleteApplicationKeyResponse{
DeletionDate: timestamppb.New(deletionDetails.EventDate),
}, nil
}

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -0,0 +1,206 @@
//go:build integration
package app_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func TestCreateApplicationKey(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
t.Parallel()
tt := []struct {
testName string
creationRequest *app.CreateApplicationKeyRequest
inputCtx context.Context
expectedErrorType codes.Code
}{
{
testName: "when app id is not found should return failed precondition",
inputCtx: IAMOwnerCtx,
creationRequest: &app.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
AppId: gofakeit.UUID(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
expectedErrorType: codes.FailedPrecondition,
},
{
testName: "when CreateAPIApp request is valid should create app and return no error",
inputCtx: IAMOwnerCtx,
creationRequest: &app.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
AppId: createdApp.GetAppId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
},
// LoginUser
{
testName: "when user has no project.app.write permission for app key generation should return permission error",
inputCtx: LoginUserCtx,
creationRequest: &app.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
AppId: createdApp.GetAppId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
expectedErrorType: codes.PermissionDenied,
},
// OrgOwner
{
testName: "when user is OrgOwner app key request should succeed",
inputCtx: OrgOwnerCtx,
creationRequest: &app.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
AppId: createdApp.GetAppId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
},
// ProjectOwner
{
testName: "when user is ProjectOwner app key request should succeed",
inputCtx: projectOwnerCtx,
creationRequest: &app.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
AppId: createdApp.GetAppId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
res, err := instance.Client.AppV2Beta.CreateApplicationKey(tc.inputCtx, tc.creationRequest)
require.Equal(t, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
assert.NotZero(t, res.GetId())
assert.NotZero(t, res.GetCreationDate())
assert.NotZero(t, res.GetKeyDetails())
}
})
}
}
func TestDeleteApplicationKey(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
t.Parallel()
tt := []struct {
testName string
deletionRequest func(ttt *testing.T) *app.DeleteApplicationKeyRequest
inputCtx context.Context
expectedErrorType codes.Code
}{
{
testName: "when app key ID is not found should return not found error",
inputCtx: IAMOwnerCtx,
deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest {
return &app.DeleteApplicationKeyRequest{
Id: gofakeit.UUID(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetAppId(),
}
},
expectedErrorType: codes.NotFound,
},
{
testName: "when valid app key ID should delete successfully",
inputCtx: IAMOwnerCtx,
deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1))
return &app.DeleteApplicationKeyRequest{
Id: createdAppKey.GetId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetAppId(),
}
},
},
// LoginUser
{
testName: "when user has no project.app.write permission for app key deletion should return permission error",
inputCtx: LoginUserCtx,
deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1))
return &app.DeleteApplicationKeyRequest{
Id: createdAppKey.GetId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetAppId(),
}
},
expectedErrorType: codes.PermissionDenied,
},
// ProjectOwner
{
testName: "when user is OrgOwner API request should succeed",
inputCtx: projectOwnerCtx,
deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1))
return &app.DeleteApplicationKeyRequest{
Id: createdAppKey.GetId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetAppId(),
}
},
},
// OrganizationOwner
{
testName: "when user is OrgOwner app key deletion request should succeed",
inputCtx: OrgOwnerCtx,
deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1))
return &app.DeleteApplicationKeyRequest{
Id: createdAppKey.GetId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetAppId(),
}
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
deletionReq := tc.deletionRequest(t)
// When
res, err := instance.Client.AppV2Beta.DeleteApplicationKey(tc.inputCtx, deletionReq)
// Then
require.Equal(t, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
assert.NotEmpty(t, res.GetDeletionDate())
}
})
}
}

View File

@@ -1,6 +1,6 @@
//go:build integration
package instance_test
package app_test
import (
"context"
@@ -653,9 +653,9 @@ func TestUpdateApplication_WithDifferentPermissions(t *testing.T) {
})
require.Nil(t, appNameChangeErr)
appForAPIConfigChangeForProjectOwner := createAPIApp(t, p.GetId())
appForAPIConfigChangeForOrgOwner := createAPIApp(t, p.GetId())
appForAPIConfigChangeForLoginUser := createAPIApp(t, p.GetId())
appForAPIConfigChangeForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appForAPIConfigChangeForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appForAPIConfigChangeForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appForOIDCConfigChangeForProjectOwner := createOIDCApp(t, baseURI, p.GetId())
appForOIDCConfigChangeForOrgOwner := createOIDCApp(t, baseURI, p.GetId())
@@ -914,9 +914,9 @@ func TestDeleteApplication(t *testing.T) {
func TestDeleteApplication_WithDifferentPermissions(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
appToDeleteForLoginUser := createAPIApp(t, p.GetId())
appToDeleteForProjectOwner := createAPIApp(t, p.GetId())
appToDeleteForOrgOwner := createAPIApp(t, p.GetId())
appToDeleteForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appToDeleteForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appToDeleteForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
t.Parallel()
tt := []struct {
@@ -1035,9 +1035,9 @@ func TestDeactivateApplication(t *testing.T) {
func TestDeactivateApplication_WithDifferentPermissions(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
appToDeactivateForLoginUser := createAPIApp(t, p.GetId())
appToDeactivateForPrjectOwner := createAPIApp(t, p.GetId())
appToDeactivateForOrgOwner := createAPIApp(t, p.GetId())
appToDeactivateForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appToDeactivateForPrjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
appToDeactivateForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
t.Parallel()
@@ -1162,13 +1162,13 @@ func TestReactivateApplication(t *testing.T) {
func TestReactivateApplication_WithDifferentPermissions(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
appToReactivateForLoginUser := createAPIApp(t, p.GetId())
appToReactivateForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
deactivateApp(t, appToReactivateForLoginUser, p.GetId())
appToReactivateForProjectOwner := createAPIApp(t, p.GetId())
appToReactivateForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
deactivateApp(t, appToReactivateForProjectOwner, p.GetId())
appToReactivateForOrgOwner := createAPIApp(t, p.GetId())
appToReactivateForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
deactivateApp(t, appToReactivateForOrgOwner, p.GetId())
t.Parallel()
@@ -1342,9 +1342,9 @@ func TestRegenerateClientSecret(t *testing.T) {
func TestRegenerateClientSecret_WithDifferentPermissions(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
apiAppToRegenForLoginUser := createAPIApp(t, p.GetId())
apiAppToRegenForProjectOwner := createAPIApp(t, p.GetId())
apiAppToRegenForOrgOwner := createAPIApp(t, p.GetId())
apiAppToRegenForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
apiAppToRegenForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
apiAppToRegenForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
oidcAppToRegenForLoginUser := createOIDCApp(t, baseURI, p.GetId())
oidcAppToRegenForProjectOwner := createOIDCApp(t, baseURI, p.GetId())

View File

@@ -1,6 +1,6 @@
//go:build integration
package instance_test
package app_test
import (
"context"
@@ -165,9 +165,9 @@ func TestListApplications(t *testing.T) {
t.Parallel()
createdApiApp, apiAppName := createAPIAppWithName(t, p.GetId())
createdApiApp, apiAppName := createAPIAppWithName(t, IAMOwnerCtx, instance, p.GetId())
createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, p.GetId())
createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, IAMOwnerCtx, instance, p.GetId())
deactivateApp(t, createdDeactivatedApiApp, p.GetId())
_, createdSAMLApp, samlAppName := createSAMLAppWithName(t, gofakeit.URL(), p.GetId())
@@ -573,3 +573,248 @@ func TestListApplications_WithPermissionV2(t *testing.T) {
})
}
}
func TestGetApplicationKey(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApiApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
createdAppKey := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp.GetAppId(), time.Now().AddDate(0, 0, 1))
t.Parallel()
tt := []struct {
testName string
inputRequest *app.GetApplicationKeyRequest
inputCtx context.Context
expectedErrorType codes.Code
expectedAppKeyID string
}{
{
testName: "when unknown app ID should return not found error",
inputCtx: IAMOwnerCtx,
inputRequest: &app.GetApplicationKeyRequest{
Id: gofakeit.Sentence(2),
},
expectedErrorType: codes.NotFound,
},
{
testName: "when user has no permission should return membership not found error",
inputCtx: NoPermissionCtx,
inputRequest: &app.GetApplicationKeyRequest{
Id: createdAppKey.GetId(),
},
expectedErrorType: codes.NotFound,
},
{
testName: "when providing API app ID should return valid API app result",
inputCtx: projectOwnerCtx,
inputRequest: &app.GetApplicationKeyRequest{
Id: createdAppKey.GetId(),
},
expectedAppKeyID: createdAppKey.GetId(),
},
{
testName: "when user is OrgOwner should return request key",
inputCtx: OrgOwnerCtx,
inputRequest: &app.GetApplicationKeyRequest{
Id: createdAppKey.GetId(),
ProjectId: p.GetId(),
},
expectedAppKeyID: createdAppKey.GetId(),
},
{
testName: "when user is IAMOwner should return request key",
inputCtx: OrgOwnerCtx,
inputRequest: &app.GetApplicationKeyRequest{
Id: createdAppKey.GetId(),
OrganizationId: instance.DefaultOrg.GetId(),
},
expectedAppKeyID: createdAppKey.GetId(),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instance.Client.AppV2Beta.GetApplicationKey(tc.inputCtx, tc.inputRequest)
// Then
require.Equal(t, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
assert.Equal(t, tc.expectedAppKeyID, res.GetId())
assert.NotEmpty(t, res.GetCreationDate())
assert.NotEmpty(t, res.GetExpirationDate())
}
}, retryDuration, tick)
})
}
}
func TestListApplicationKeys(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApiApp1 := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
createdApiApp2 := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
tomorrow := time.Now().AddDate(0, 0, 1)
in2Days := tomorrow.AddDate(0, 0, 1)
in3Days := in2Days.AddDate(0, 0, 1)
appKey1 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetAppId(), in2Days)
appKey2 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetAppId(), in3Days)
appKey3 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetAppId(), tomorrow)
appKey4 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp2.GetAppId(), tomorrow)
t.Parallel()
tt := []struct {
testName string
inputRequest *app.ListApplicationKeysRequest
deps func() (projectID, applicationID, organizationID string)
inputCtx context.Context
expectedErrorType codes.Code
expectedAppKeysIDs []string
}{
{
testName: "when sorting by expiration date should return keys sorted by expiration date ascending",
inputCtx: LoginUserCtx,
inputRequest: &app.ListApplicationKeysRequest{
ResourceId: &app.ListApplicationKeysRequest_ProjectId{ProjectId: p.GetId()},
Pagination: &filter.PaginationRequest{Asc: true},
SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION,
},
expectedAppKeysIDs: []string{appKey3.GetId(), appKey4.GetId(), appKey1.GetId(), appKey2.GetId()},
},
{
testName: "when sorting by creation date should return keys sorted by creation date descending",
inputCtx: IAMOwnerCtx,
inputRequest: &app.ListApplicationKeysRequest{
ResourceId: &app.ListApplicationKeysRequest_ProjectId{ProjectId: p.GetId()},
SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE,
},
expectedAppKeysIDs: []string{appKey4.GetId(), appKey3.GetId(), appKey2.GetId(), appKey1.GetId()},
},
{
testName: "when filtering by app ID should return keys matching app ID sorted by ID",
inputCtx: projectOwnerCtx,
inputRequest: &app.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
ResourceId: &app.ListApplicationKeysRequest_ApplicationId{ApplicationId: createdApiApp1.GetAppId()},
},
expectedAppKeysIDs: []string{appKey1.GetId(), appKey2.GetId(), appKey3.GetId()},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instance.Client.AppV2Beta.ListApplicationKeys(tc.inputCtx, tc.inputRequest)
// Then
require.Equal(ttt, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
require.Len(ttt, res.GetKeys(), len(tc.expectedAppKeysIDs))
for i, k := range res.GetKeys() {
assert.Equal(ttt, tc.expectedAppKeysIDs[i], k.GetId())
}
}
}, retryDuration, tick)
})
}
}
func TestListApplicationKeys_WithPermissionV2(t *testing.T) {
ensureFeaturePermissionV2Enabled(t, instancePermissionV2)
iamOwnerCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
loginUserCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeLogin)
p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
createdApiApp1 := createAPIApp(t, iamOwnerCtx, instancePermissionV2, p.GetId())
createdApiApp2 := createAPIApp(t, iamOwnerCtx, instancePermissionV2, p.GetId())
tomorrow := time.Now().AddDate(0, 0, 1)
in2Days := tomorrow.AddDate(0, 0, 1)
in3Days := in2Days.AddDate(0, 0, 1)
appKey1 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetAppId(), in2Days)
appKey2 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetAppId(), in3Days)
appKey3 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetAppId(), tomorrow)
appKey4 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp2.GetAppId(), tomorrow)
t.Parallel()
tt := []struct {
testName string
inputRequest *app.ListApplicationKeysRequest
deps func() (projectID, applicationID, organizationID string)
inputCtx context.Context
expectedErrorType codes.Code
expectedAppKeysIDs []string
}{
{
testName: "when sorting by expiration date should return keys sorted by expiration date ascending",
inputCtx: loginUserCtx,
inputRequest: &app.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION,
},
expectedAppKeysIDs: []string{appKey3.GetId(), appKey4.GetId(), appKey1.GetId(), appKey2.GetId()},
},
{
testName: "when sorting by creation date should return keys sorted by creation date descending",
inputCtx: iamOwnerCtx,
inputRequest: &app.ListApplicationKeysRequest{
SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE,
},
expectedAppKeysIDs: []string{appKey4.GetId(), appKey3.GetId(), appKey2.GetId(), appKey1.GetId()},
},
{
testName: "when filtering by app ID should return keys matching app ID sorted by ID",
inputCtx: projectOwnerCtx,
inputRequest: &app.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
ResourceId: &app.ListApplicationKeysRequest_ApplicationId{ApplicationId: createdApiApp1.GetAppId()},
},
expectedAppKeysIDs: []string{appKey1.GetId(), appKey2.GetId(), appKey3.GetId()},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instancePermissionV2.Client.AppV2Beta.ListApplicationKeys(tc.inputCtx, tc.inputRequest)
// Then
require.Equal(ttt, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
require.Len(ttt, res.GetKeys(), len(tc.expectedAppKeysIDs))
for i, k := range res.GetKeys() {
assert.Equal(ttt, tc.expectedAppKeysIDs[i], k.GetId())
}
}
}, retryDuration, tick)
})
}
}

View File

@@ -1,6 +1,6 @@
//go:build integration
package instance_test
package app_test
import (
"context"
@@ -13,6 +13,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
@@ -150,14 +151,14 @@ func createOIDCApp(t *testing.T, baseURI, projctID string) *app.CreateApplicatio
return app
}
func createAPIAppWithName(t *testing.T, projectID string) (*app.CreateApplicationResponse, string) {
func createAPIAppWithName(t *testing.T, ctx context.Context, inst *integration.Instance, projectID string) (*app.CreateApplicationResponse, string) {
appName := gofakeit.AppName()
reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{
ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
}
appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
appForAPIConfigChange, appAPIConfigChangeErr := inst.Client.AppV2Beta.CreateApplication(ctx, &app.CreateApplicationRequest{
ProjectId: projectID,
Name: appName,
CreationRequestType: reqForAPIAppCreation,
@@ -167,8 +168,8 @@ func createAPIAppWithName(t *testing.T, projectID string) (*app.CreateApplicatio
return appForAPIConfigChange, appName
}
func createAPIApp(t *testing.T, projectID string) *app.CreateApplicationResponse {
res, _ := createAPIAppWithName(t, projectID)
func createAPIApp(t *testing.T, ctx context.Context, inst *integration.Instance, projectID string) *app.CreateApplicationResponse {
res, _ := createAPIAppWithName(t, ctx, inst, projectID)
return res
}
@@ -203,3 +204,17 @@ func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instan
assert.True(tt, f.PermissionCheckV2.GetEnabled())
}, retryDuration, tick, "timed out waiting for ensuring instance feature")
}
func createAppKey(t *testing.T, ctx context.Context, inst *integration.Instance, projectID, appID string, expirationDate time.Time) *app.CreateApplicationKeyResponse {
res, err := inst.Client.AppV2Beta.CreateApplicationKey(ctx,
&app.CreateApplicationKeyRequest{
AppId: appID,
ProjectId: projectID,
ExpirationDate: timestamppb.New(expirationDate.UTC()),
},
)
require.Nil(t, err)
return res
}

View File

@@ -2,9 +2,13 @@ package app
import (
"context"
"strings"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert"
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
@@ -35,3 +39,38 @@ func (s *Server) ListApplications(ctx context.Context, req *app.ListApplications
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
}, nil
}
func (s *Server) GetApplicationKey(ctx context.Context, req *app.GetApplicationKeyRequest) (*app.GetApplicationKeyResponse, error) {
queries, err := convert.GetApplicationKeyQueriesRequestToDomain(req.GetOrganizationId(), req.GetProjectId(), req.GetApplicationId())
if err != nil {
return nil, err
}
key, err := s.query.GetAuthNKeyByIDWithPermission(ctx, true, strings.TrimSpace(req.GetId()), s.checkPermission, queries...)
if err != nil {
return nil, err
}
return &app.GetApplicationKeyResponse{
Id: key.ID,
CreationDate: timestamppb.New(key.CreationDate),
ExpirationDate: timestamppb.New(key.Expiration),
}, nil
}
func (s *Server) ListApplicationKeys(ctx context.Context, req *app.ListApplicationKeysRequest) (*app.ListApplicationKeysResponse, error) {
queries, err := convert.ListApplicationKeysRequestToDomain(s.systemDefaults, req)
if err != nil {
return nil, err
}
res, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterUnspecified, s.checkPermission)
if err != nil {
return nil, err
}
return &app.ListApplicationKeysResponse{
Keys: convert.ApplicationKeysToPb(res.AuthNKeys),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
}, nil
}

View File

@@ -177,7 +177,7 @@ func AddAPIClientKeyRequestToDomain(key *mgmt_pb.AddAppKeyRequest) *domain.Appli
}
func ListAPIClientKeysRequestToQuery(ctx context.Context, req *mgmt_pb.ListAppKeysRequest) (*query.AuthNKeySearchQueries, error) {
resourcOwner, err := query.NewAuthNKeyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID)
resourceOwner, err := query.NewAuthNKeyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
@@ -197,7 +197,7 @@ func ListAPIClientKeysRequestToQuery(ctx context.Context, req *mgmt_pb.ListAppKe
Asc: asc,
},
Queries: []query.SearchQuery{
resourcOwner,
resourceOwner,
projectID,
appID,
},

View File

@@ -38,6 +38,11 @@ func (c *Commands) AddApplicationKey(ctx context.Context, key *domain.Applicatio
if err != nil {
return nil, err
}
if resourceOwner == "" {
resourceOwner = application.ResourceOwner
}
if !application.State.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-sak25", "Errors.Project.App.NotFound")
}
@@ -59,6 +64,10 @@ func (c *Commands) addApplicationKey(ctx context.Context, key *domain.Applicatio
return nil, err
}
if err := c.checkPermissionUpdateApplication(ctx, keyWriteModel.ResourceOwner, keyWriteModel.AggregateID); err != nil {
return nil, err
}
if !keyWriteModel.KeysAllowed {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Dff54", "Errors.Project.App.AuthMethodNoPrivateKeyJWT")
}
@@ -110,6 +119,10 @@ func (c *Commands) RemoveApplicationKey(ctx context.Context, projectID, applicat
return nil, zerrors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.Project.App.Key.NotFound")
}
if err := c.checkPermissionUpdateApplication(ctx, keyWriteModel.ResourceOwner, keyWriteModel.AggregateID); err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationKeyRemovedEvent(ctx, ProjectAggregateFromWriteModel(&keyWriteModel.WriteModel), keyID))
if err != nil {
return nil, err

View File

@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
permissionmock "github.com/zitadel/zitadel/internal/domain/mock"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/id"
@@ -17,9 +18,10 @@ import (
func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
keySize int
eventstore func(*testing.T) *eventstore.Eventstore
idGenerator id.Generator
keySize int
permissionCheckMock domain.PermissionCheck
}
type args struct {
ctx context.Context
@@ -39,9 +41,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
{
name: "no aggregateid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
permissionCheckMock: permissionmock.MockPermissionCheckOK(),
},
args: args{
ctx: context.Background(),
@@ -57,9 +58,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
{
name: "no appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
permissionCheckMock: permissionmock.MockPermissionCheckOK(),
},
args: args{
ctx: context.Background(),
@@ -77,10 +77,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
eventstore: expectEventstore(expectFilter()),
permissionCheckMock: permissionmock.MockPermissionCheckOK(),
},
args: args{
ctx: context.Background(),
@@ -97,10 +95,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
},
},
{
name: "create key not allowed, precondition error",
name: "create key not allowed, precondition error 1",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
@@ -121,7 +118,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
permissionCheckMock: permissionmock.MockPermissionCheckOK(),
},
args: args{
ctx: context.Background(),
@@ -138,10 +136,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
},
},
{
name: "create key not allowed, precondition error",
name: "permission check failed",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
@@ -162,8 +159,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
keySize: 10,
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
keySize: 10,
permissionCheckMock: permissionmock.MockPermissionCheckErr(zerrors.ThrowPermissionDenied(nil, "mock.err", "mock permission check failed")),
},
args: args{
ctx: context.Background(),
@@ -175,6 +173,47 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "create key not allowed, precondition error 2",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
),
expectFilter(
eventFromEventPusher(
project.NewAPIConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"client1@project",
"secret",
domain.APIAuthMethodTypeBasic),
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
keySize: 10,
permissionCheckMock: permissionmock.MockPermissionCheckOK(),
},
args: args{
ctx: context.Background(),
key: &domain.ApplicationKey{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
ApplicationID: "app1",
},
},
res: res{
err: zerrors.IsPreconditionFailed,
},
@@ -183,9 +222,10 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
applicationKeySize: tt.fields.keySize,
checkPermission: tt.fields.permissionCheckMock,
}
got, err := r.AddApplicationKey(tt.args.ctx, tt.args.key, tt.args.resourceOwner)
if tt.res.err == nil {

View File

@@ -0,0 +1,22 @@
package permissionmock
import (
"golang.org/x/net/context"
"github.com/zitadel/zitadel/internal/domain"
)
// MockPermissionCheckErr returns a permission check function that will fail
// and return the input error
func MockPermissionCheckErr(err error) domain.PermissionCheck {
return func(_ context.Context, _, _, _ string) error {
return err
}
}
// MockPermissionCheckOK returns a permission check function that will succeed
func MockPermissionCheckOK() domain.PermissionCheck {
return func(_ context.Context, _, _, _ string) (err error) {
return nil
}
}

View File

@@ -98,6 +98,7 @@ type AuthNKey struct {
ChangeDate time.Time
ResourceOwner string
Sequence uint64
ApplicationID string
Expiration time.Time
Type domain.AuthNKeyType
@@ -222,6 +223,19 @@ func (q *Queries) SearchAuthNKeysData(ctx context.Context, queries *AuthNKeySear
return authNKeys, err
}
func (q *Queries) GetAuthNKeyByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck, queries ...SearchQuery) (*AuthNKey, error) {
key, err := q.GetAuthNKeyByID(ctx, shouldTriggerBulk, id, queries...)
if err != nil {
return nil, err
}
if err := appCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck); err != nil {
return nil, err
}
return key, nil
}
func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (key *AuthNKey, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -330,6 +344,7 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
AuthNKeyColumnSequence.identifier(),
AuthNKeyColumnExpiration.identifier(),
AuthNKeyColumnType.identifier(),
AuthNKeyColumnObjectID.identifier(),
countColumn.identifier(),
).From(authNKeyTable.identifier()).
PlaceholderFormat(sq.Dollar)
@@ -348,6 +363,7 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
&authNKey.Sequence,
&authNKey.Expiration,
&authNKey.Type,
&authNKey.ApplicationID,
&count,
)
if err != nil {

View File

@@ -26,6 +26,7 @@ var (
` projections.authn_keys2.sequence,` +
` projections.authn_keys2.expiration,` +
` projections.authn_keys2.type,` +
` projections.authn_keys2.object_id,` +
` COUNT(*) OVER ()` +
` FROM projections.authn_keys2`
prepareAuthNKeysCols = []string{
@@ -37,6 +38,7 @@ var (
"sequence",
"expiration",
"type",
"object_id",
"count",
}
@@ -129,6 +131,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
uint64(20211109),
testNow,
1,
"app1",
},
},
),
@@ -147,6 +150,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
Sequence: 20211109,
Expiration: testNow,
Type: domain.AuthNKeyTypeJSON,
ApplicationID: "app1",
},
},
},
@@ -168,6 +172,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
uint64(20211109),
testNow,
1,
"app1",
},
{
"id-2",
@@ -178,6 +183,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
uint64(20211109),
testNow,
1,
"app1",
},
},
),
@@ -196,6 +202,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
Sequence: 20211109,
Expiration: testNow,
Type: domain.AuthNKeyTypeJSON,
ApplicationID: "app1",
},
{
ID: "id-2",
@@ -206,6 +213,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
Sequence: 20211109,
Expiration: testNow,
Type: domain.AuthNKeyTypeJSON,
ApplicationID: "app1",
},
},
},