diff --git a/internal/api/grpc/app/v2beta/app_key.go b/internal/api/grpc/app/v2beta/app_key.go new file mode 100644 index 0000000000..8c0c1989b2 --- /dev/null +++ b/internal/api/grpc/app/v2beta/app_key.go @@ -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 +} diff --git a/internal/api/grpc/app/v2beta/convert/api_app.go b/internal/api/grpc/app/v2beta/convert/api_app.go index bad76ab0d5..4900d534cb 100644 --- a/internal/api/grpc/app/v2beta/convert/api_app.go +++ b/internal/api/grpc/app/v2beta/convert/api_app.go @@ -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 +} diff --git a/internal/api/grpc/app/v2beta/convert/api_app_test.go b/internal/api/grpc/app/v2beta/convert/api_app_test.go index 9f15c3df76..dcb87d712f 100644 --- a/internal/api/grpc/app/v2beta/convert/api_app_test.go +++ b/internal/api/grpc/app/v2beta/convert/api_app_test.go @@ -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) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/convert.go b/internal/api/grpc/app/v2beta/convert/convert.go index c732b3a0c5..a0a1d5ef05 100644 --- a/internal/api/grpc/app/v2beta/convert/convert.go +++ b/internal/api/grpc/app/v2beta/convert/convert.go @@ -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 +} diff --git a/internal/api/grpc/app/v2beta/convert/convert_test.go b/internal/api/grpc/app/v2beta/convert/convert_test.go index 5835691d43..8715d2a5dd 100644 --- a/internal/api/grpc/app/v2beta/convert/convert_test.go +++ b/internal/api/grpc/app/v2beta/convert/convert_test.go @@ -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) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/app_key_test.go b/internal/api/grpc/app/v2beta/integration_test/app_key_test.go new file mode 100644 index 0000000000..7c3c886cff --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/app_key_test.go @@ -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()) + } + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/app_test.go b/internal/api/grpc/app/v2beta/integration_test/app_test.go index 1ba46987cf..67e59aa91d 100644 --- a/internal/api/grpc/app/v2beta/integration_test/app_test.go +++ b/internal/api/grpc/app/v2beta/integration_test/app_test.go @@ -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()) diff --git a/internal/api/grpc/app/v2beta/integration_test/query_test.go b/internal/api/grpc/app/v2beta/integration_test/query_test.go index 578fcec138..4f6679da7f 100644 --- a/internal/api/grpc/app/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/app/v2beta/integration_test/query_test.go @@ -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) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/server_test.go b/internal/api/grpc/app/v2beta/integration_test/server_test.go index 6618ab0616..8ba012c18b 100644 --- a/internal/api/grpc/app/v2beta/integration_test/server_test.go +++ b/internal/api/grpc/app/v2beta/integration_test/server_test.go @@ -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 +} diff --git a/internal/api/grpc/app/v2beta/query.go b/internal/api/grpc/app/v2beta/query.go index add8af83e6..2926884520 100644 --- a/internal/api/grpc/app/v2beta/query.go +++ b/internal/api/grpc/app/v2beta/query.go @@ -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 +} diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go index 186cedc933..fa31565445 100644 --- a/internal/api/grpc/management/project_application_converter.go +++ b/internal/api/grpc/management/project_application_converter.go @@ -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, }, diff --git a/internal/command/project_application_key.go b/internal/command/project_application_key.go index 519e9fc30a..47dacdd638 100644 --- a/internal/command/project_application_key.go +++ b/internal/command/project_application_key.go @@ -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 diff --git a/internal/command/project_application_key_test.go b/internal/command/project_application_key_test.go index 9fd46c75f3..3402cab507 100644 --- a/internal/command/project_application_key_test.go +++ b/internal/command/project_application_key_test.go @@ -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 { diff --git a/internal/domain/mock/permission.go b/internal/domain/mock/permission.go new file mode 100644 index 0000000000..9a3c6478c9 --- /dev/null +++ b/internal/domain/mock/permission.go @@ -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 + } +} diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 5a1f49d63c..abda5f011e 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -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 { diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index ce45185363..619ffaac8c 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -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", }, }, }, diff --git a/proto/zitadel/app/v2beta/app.proto b/proto/zitadel/app/v2beta/app.proto index f85e3c021d..f108f3bacb 100644 --- a/proto/zitadel/app/v2beta/app.proto +++ b/proto/zitadel/app/v2beta/app.proto @@ -92,3 +92,30 @@ message ApplicationNameQuery { } ]; } + +enum ApplicationKeysSorting { + APPLICATION_KEYS_SORT_BY_ID = 0; + APPLICATION_KEYS_SORT_BY_PROJECT_ID = 1; + APPLICATION_KEYS_SORT_BY_APPLICATION_ID = 2; + APPLICATION_KEYS_SORT_BY_CREATION_DATE = 3; + APPLICATION_KEYS_SORT_BY_ORGANIZATION_ID = 4; + APPLICATION_KEYS_SORT_BY_EXPIRATION = 5; + APPLICATION_KEYS_SORT_BY_TYPE = 6; +} + +message ApplicationKey { + string id = 1; + string application_id = 2; + string project_id = 3; + google.protobuf.Timestamp creation_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + string organization_id = 5; + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/app_service.proto b/proto/zitadel/app/v2beta/app_service.proto index a881022caa..61cde73696 100644 --- a/proto/zitadel/app/v2beta/app_service.proto +++ b/proto/zitadel/app/v2beta/app_service.proto @@ -114,8 +114,6 @@ service AppService { // // Create an application. The application can be OIDC, API or SAML type, based on the input. // - // The user needs to have project.app.write permission - // // Required permissions: // - project.app.write rpc CreateApplication(CreateApplicationRequest) returns (CreateApplicationResponse) { @@ -145,8 +143,6 @@ service AppService { // Changes the configuration of an OIDC, API or SAML type application, as well as // the application name, based on the input provided. // - // The user needs to have project.app.write permission - // // Required permissions: // - project.app.write rpc UpdateApplication(UpdateApplicationRequest) returns (UpdateApplicationResponse) { @@ -175,8 +171,6 @@ service AppService { // // Retrieves the application matching the provided ID. // - // The user needs to have project.app.read permission - // // Required permissions: // - project.app.read rpc GetApplication(GetApplicationRequest) returns (GetApplicationResponse) { @@ -203,9 +197,7 @@ service AppService { // Delete Application // // Deletes the application belonging to the input project and matching the provided - // application ID - // - // The user needs to have project.app.delete permission + // application ID. // // Required permissions: // - project.app.delete @@ -233,9 +225,7 @@ service AppService { // Deactivate Application // // Deactivates the application belonging to the input project and matching the provided - // application ID - // - // The user needs to have project.app.write permission + // application ID. // // Required permissions: // - project.app.write @@ -264,9 +254,7 @@ service AppService { // Reactivate Application // // Reactivates the application belonging to the input project and matching the provided - // application ID - // - // The user needs to have project.app.write permission + // application ID. // // Required permissions: // - project.app.write @@ -297,8 +285,6 @@ service AppService { // // Regenerates the client secret of an API or OIDC application that belongs to the input project. // - // The user needs to have project.app.write permission - // // Required permissions: // - project.app.write rpc RegenerateClientSecret(RegenerateClientSecretRequest) returns (RegenerateClientSecretResponse) { @@ -331,8 +317,6 @@ service AppService { // The result can be sorted by app id, name, creation date, change date or state. It can also // be filtered by app state, app type and app name. // - // The user needs to have project.app.read permission - // // Required permissions: // - project.app.read rpc ListApplications(ListApplicationsRequest) returns (ListApplicationsResponse) { @@ -356,6 +340,129 @@ service AppService { } }; } + + + // Create Application Key + // + // Create a new application key, which is used to authorize an API application. + // + // Key details are returned in the response. They must be stored safely, as it will not + // be possible to retrieve them again. + // + // Required permissions: + // - `project.app.write` + rpc CreateApplicationKey(CreateApplicationKeyRequest) returns (CreateApplicationKeyResponse) { + option (google.api.http) = { + post: "/v2beta/application_keys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The created application key"; + } + }; + }; + } + + // Delete Application Key + // + // Deletes an application key matching the provided ID. + // + // Organization ID is not mandatory, but helps with filtering/performance. + // + // The deletion time is returned in response message. + // + // Required permissions: + // - `project.app.write` + rpc DeleteApplicationKey(DeleteApplicationKeyRequest) returns (DeleteApplicationKeyResponse) { + option (google.api.http) = { + delete: "/v2beta/application_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of deletion."; + } + }; + }; + } + + // Get Application Key + // + // Retrieves the application key matching the provided ID. + // + // Specifying a project, organization and app ID is optional but help with filtering/performance. + // + // Required permissions: + // - project.app.read + rpc GetApplicationKey(GetApplicationKeyRequest) returns (GetApplicationKeyResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The fetched app key."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/application_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Application Keys + // + // Returns a list of application keys matching the input parameters. + // + // The result can be sorted by id, aggregate, creation date, expiration date, resource owner or type. + // It can also be filtered by app, project or organization ID. + // + // Required permissions: + // - project.app.read + rpc ListApplicationKeys(ListApplicationKeysRequest) returns (ListApplicationKeysResponse) { + option (google.api.http) = { + post: "/v2beta/application_keys/search" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The matching applications"; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } } message CreateApplicationRequest { @@ -785,4 +892,103 @@ message ListApplicationsResponse { // Contains the total number of apps matching the query and the applied limit. zitadel.filter.v2.PaginationResponse pagination = 2; -} \ No newline at end of file +} + +message CreateApplicationKeyRequest { + string app_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + string project_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + // The date the key will expire + google.protobuf.Timestamp expiration_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + description: "The date the key will expire"; + } + ]; +} + +message CreateApplicationKeyResponse { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"28746028909593987\""; + } + ]; + + // The timestamp of the app creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + bytes key_details = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"eyJ0eXBlIjoiYXBwbGljYXRpb24iLCJrZXlJZCI6IjIwMjcxMDE4NjYyMjcxNDExMyIsImtleSI6Ii0tLS0tQkVHSU4gUlNBIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUVvd0lCQUFLQ0FRRUFuMUxyNStTV0pGRllURU1kaXQ2U0dNY0E2Yks5dG0xMmhlcm55V0wrZm9PWnA3eEVcbk9wcmsvWE81QVplSU5NY0x0ZVhxckJlK1NPdVVNMFpLU2xCMHFTNzNjVStDVTVMTGoycVB0UzhNOFI0N3BGdFhcbjJXRTFJNjNhZHB1N01TejA2SXduQ2lyNnJYOTVPQ2ZneHA3VU1Dd0pSTUZmYXJqdjVBRXY3NXpsSS9lYUV6bUJcbkxKWU1xanZFRmZoN2x3M2lPT3VsWW9kNjNpN3RDNWl5czNlYjNLZW4yWU0rN1FSbXB2dE5qcTJMVmlIMnkrUGJcbk9ESlI3MU9ib05TYVJDNTZDUFpWVytoWDByYXI3VzMwUjI2eGtIQ09oSytQbUpSeGtOY0g1VTdja0xXMEw0WEVcbnNNZkVUSmszeDR3Q0psbisxbElXUzkrNmw0R1E2TWRzWURyOU5RSURBUUFCQW9JQkFCSkx6WGQxMHFBZEQwekNcbnNGUFFOMnJNLzVmV3hONThONDR0YWF6QXg0VHp5K050UlZDTmxScGQvYkxuR2VjbHJIeVpDSmYycWcxcHNEMHJcbkowRGRlR2d0VXBFYWxsYk9scjNEZVBsUGkrYnNsK0RKOUk2c0VSUWwxTjZtQjVzZ0ZJZllBR3UwZjlFSXdIem9cblozR25yNnBRaEVmM0JPUVdsTVhVTlJNSksyOHp3M2E1L01nRmtKVUZUSTUzeXFwbGRtZ2hLajRZR1hLRk1LUGhcbkV3RkxrRncwK2s3K0xuSjFQNGp1ZVd1RXo3WlAyaFpvUWxCcXdSajVyTG9QZ05RbUU4UytFVDRuczlUYzByOFFcbnFyaHlacDZBczJrTDhGTytCZnF3SVpDZnpnWHN2cC9PLzRaSHIzVTB2Ymp3UW1sSzdVSm42U0J6T2hpWFpNU0lcbk5Wc0V5VUVDZ1lFQTFEaktkRGo3NTM1MWQzdlRNQlRFd2JSQ3hoUVZOdENFMnMwVUw4ckJQZ1I0K1dlblNUWmFcbnprWUprcEV0bE54VGxzYnN1Y0RTUXZqeWRYYk5nSHFBeDYzMm1vdTVkak9lR0VTUDFWVGtUdElsZFZQZWszQWxcbjVYbkpQa1dqWGVyVVJZNm5KeUQ5UWhlREx3MVp4NEFYVzNHWURiTFkrT05XV0VKUlJaQUloNjBDZ1lFQXdEQ2xcbnc1MHc4dkcvbEJ4RzNSYW9FaHdLOWNna1VXOHk2T25DekNwcEtjOEZUUmY1VE5iWjl5TzNXUmdYajhkeHRCakFcbkl5VGlzYk9NQk1VaFZKUUtGZHRQaDhoVDBwRkRjeE9ndzY0aHBtYzhyY2RTbXVKNzlYSVRTaHUySjA0N0UvNFZcbnJOTThpWVk5ZGR3VGdGUUlsdFNZL0l0RnFxWERmdjhqK1dVY25La0NnWUVBaENOUU80bDNuNjRucWR2WnBTaHBcblVrclJBTkJrWFJyOGZkZ1BaNnFSSS9KWStNSEhjVmg4dGM3NkN0NkdTUmZlbkJVRU5LeVF2czZPK1FDZCtBOU9cbnZBWGZkRjduZldlcVdtWG1RT2g0dDNNMWk1WkxFZlpVUWt2UU9BdllLcFFhMDZ4OCsyb1pCdHZvL0pVTmY2Q0xcbjZvNFNKUVZrLzZOZGtkckpDODBnNG9rQ2dZQkZsNWYrbkVYa1F0dWZVeG5wNXRGWE5XWldsM0ZuTjMvVXpRaW5cbmkxZm5OcnB4cnhPcjJrUzA4KzdwU1FzSEdpNDNDNXRQWG9UajJlTUN1eXNWaUVHYXBuNUc2YWhJb0NjdlhWVWlcblprUnpFQUR0NERZdU5ZS3pYdXBUTkhPaUNmYmtoMlhyM2RXVzZ0QUloSGRmU1k2T3AwNzZhNmYvWWVUSGNMWGpcbkVkVHBlUUtCZ0FPdnBqcDQ4TzRUWEZkU0JLSnYya005OHVhUjlSQURtdGxTWHd2cTlyQkhTV084NFk4bzE0L1Bcbkl1UmxUOHhROGRYKzhMR21UUCtjcUtiOFFRQ1grQk1YUWxMSEVtWnpnb0xFa0pGMUVIMm4vZEZ5bngxS3prdFNcbm9UZUdsRzZhbXhVOVh4eW9RVFlEVGJCbERwc2FZUlFBZ2FUQzM3UVZRUjhmK1ZoRzFHSFFcbi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tXG4iLCJhcHBJZCI6IjIwMjcwNjM5ODgxMzg4MDU3NyIsImNsaWVudElkIjoiMjAyNzA2Mzk4ODEzOTQ2MTEzQG15dGVzdHByb2plY3QifQ==\""; + } + ]; +} + +message DeleteApplicationKeyRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string project_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string application_id = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string organization_id = 4 [(validate.rules).string = {max_len: 200}]; +} + +message DeleteApplicationKeyResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetApplicationKeyRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string project_id = 2 [(validate.rules).string = {max_len: 200}]; + string application_id = 3 [(validate.rules).string = {max_len: 200}]; + string organization_id = 4 [(validate.rules).string = {max_len: 200}]; +} + +message GetApplicationKeyResponse { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // the date a key will expire + google.protobuf.Timestamp expiration_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the date a key will expire"; + example: "\"3019-04-01T08:45:00.000000Z\""; + } + ]; +} + +message ListApplicationKeysRequest { + // Pagination and sorting. + zitadel.filter.v2.PaginationRequest pagination = 1; + + ApplicationKeysSorting sorting_column = 2; + + oneof resource_id { + string application_id = 3 [(validate.rules).string = {min_len: 1; max_len: 200}]; + string project_id = 4 [(validate.rules).string = {min_len: 1; max_len: 200}]; + string organization_id = 5 [(validate.rules).string = {min_len: 1; max_len: 200}]; + } +} + +message ListApplicationKeysResponse { + repeated ApplicationKey keys = 1; + + // Contains the total number of app keys matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 2; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 74d5dcf60b..bb62e2eba6 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -3709,6 +3709,7 @@ service ManagementService { }; } + // Deprecated: Use [GetApplicationKey](/apis/resources/application_service_v2/application-service-get-application-key.api.mdx) instead to get an application key rpc GetAppKey(GetAppKeyRequest) returns (GetAppKeyResponse) { option (google.api.http) = { get: "/projects/{project_id}/apps/{app_id}/keys/{key_id}" @@ -3731,9 +3732,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [ListApplicationKeys](/apis/resources/application_service_v2/application-service-list-application-keys.api.mdx) instead to list application keys rpc ListAppKeys(ListAppKeysRequest) returns (ListAppKeysResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/keys/_search" @@ -3760,6 +3763,8 @@ service ManagementService { }; } + // Deprecated: Use [CreateApplicationKey](/apis/resources/application_service_v2/application-service-create-application-key.api.mdx) instead to + // create an application key rpc AddAppKey(AddAppKeyRequest) returns (AddAppKeyResponse){ option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/keys" @@ -3783,9 +3788,12 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [DeleteApplicationKey](/apis/resources/application_service_v2/application-service-delete-application-key.api.mdx) instead to + // delete an application key rpc RemoveAppKey(RemoveAppKeyRequest) returns (RemoveAppKeyResponse) { option (google.api.http) = { delete: "/projects/{project_id}/apps/{app_id}/keys/{key_id}" @@ -3808,6 +3816,7 @@ service ManagementService { required: false; }; }; + deprecated: true; }; }