mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-08 16:47:40 +00:00
feat: add personal access tokens for service users (#2974)
* feat: add machine tokens * fix test * rename to pat * fix merge and tests * fix scopes * fix migration version * fix test * Update internal/repository/user/personal_access_token.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
parent
3bf9adece5
commit
699fdaf68e
@ -623,13 +623,62 @@ Generates a new machine key, details should be stored after return
|
|||||||
> **rpc** RemoveMachineKey([RemoveMachineKeyRequest](#removemachinekeyrequest))
|
> **rpc** RemoveMachineKey([RemoveMachineKeyRequest](#removemachinekeyrequest))
|
||||||
[RemoveMachineKeyResponse](#removemachinekeyresponse)
|
[RemoveMachineKeyResponse](#removemachinekeyresponse)
|
||||||
|
|
||||||
Removed a machine key
|
Removes a machine key
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DELETE: /users/{user_id}/keys/{key_id}
|
DELETE: /users/{user_id}/keys/{key_id}
|
||||||
|
|
||||||
|
|
||||||
|
### GetPersonalAccessTokenByIDs
|
||||||
|
|
||||||
|
> **rpc** GetPersonalAccessTokenByIDs([GetPersonalAccessTokenByIDsRequest](#getpersonalaccesstokenbyidsrequest))
|
||||||
|
[GetPersonalAccessTokenByIDsResponse](#getpersonalaccesstokenbyidsresponse)
|
||||||
|
|
||||||
|
Returns a personal access token of a (machine) user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GET: /users/{user_id}/pats/{token_id}
|
||||||
|
|
||||||
|
|
||||||
|
### ListPersonalAccessTokens
|
||||||
|
|
||||||
|
> **rpc** ListPersonalAccessTokens([ListPersonalAccessTokensRequest](#listpersonalaccesstokensrequest))
|
||||||
|
[ListPersonalAccessTokensResponse](#listpersonalaccesstokensresponse)
|
||||||
|
|
||||||
|
Returns all personal access tokens of a (machine) user which match the query
|
||||||
|
Limit should always be set, there is a default limit set by the service
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
POST: /users/{user_id}/pats/_search
|
||||||
|
|
||||||
|
|
||||||
|
### AddPersonalAccessToken
|
||||||
|
|
||||||
|
> **rpc** AddPersonalAccessToken([AddPersonalAccessTokenRequest](#addpersonalaccesstokenrequest))
|
||||||
|
[AddPersonalAccessTokenResponse](#addpersonalaccesstokenresponse)
|
||||||
|
|
||||||
|
Generates a new personal access token for a machine user, details should be stored after return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
POST: /users/{user_id}/pats
|
||||||
|
|
||||||
|
|
||||||
|
### RemovePersonalAccessToken
|
||||||
|
|
||||||
|
> **rpc** RemovePersonalAccessToken([RemovePersonalAccessTokenRequest](#removepersonalaccesstokenrequest))
|
||||||
|
[RemovePersonalAccessTokenResponse](#removepersonalaccesstokenresponse)
|
||||||
|
|
||||||
|
Removes a personal access token
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DELETE: /users/{user_id}/pats/{token_id}
|
||||||
|
|
||||||
|
|
||||||
### ListHumanLinkedIDPs
|
### ListHumanLinkedIDPs
|
||||||
|
|
||||||
> **rpc** ListHumanLinkedIDPs([ListHumanLinkedIDPsRequest](#listhumanlinkedidpsrequest))
|
> **rpc** ListHumanLinkedIDPs([ListHumanLinkedIDPsRequest](#listhumanlinkedidpsrequest))
|
||||||
@ -3431,6 +3480,31 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### AddPersonalAccessTokenRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| user_id | string | - | string.min_len: 1<br /> |
|
||||||
|
| expiration_date | google.protobuf.Timestamp | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### AddPersonalAccessTokenResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| token_id | string | - | |
|
||||||
|
| token | string | - | |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### AddProjectGrantMemberRequest
|
### AddProjectGrantMemberRequest
|
||||||
|
|
||||||
|
|
||||||
@ -4816,6 +4890,29 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### GetPersonalAccessTokenByIDsRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| user_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| token_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### GetPersonalAccessTokenByIDsResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| token | zitadel.user.v1.PersonalAccessToken | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### GetPreviewLabelPolicyRequest
|
### GetPreviewLabelPolicyRequest
|
||||||
This is an empty request
|
This is an empty request
|
||||||
|
|
||||||
@ -5572,6 +5669,30 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### ListPersonalAccessTokensRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| user_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| query | zitadel.v1.ListQuery | list limitations and ordering | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### ListPersonalAccessTokensResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| details | zitadel.v1.ListDetails | - | |
|
||||||
|
| result | repeated zitadel.user.v1.PersonalAccessToken | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### ListProjectChangesRequest
|
### ListProjectChangesRequest
|
||||||
|
|
||||||
|
|
||||||
@ -6525,6 +6646,29 @@ This is an empty response
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RemovePersonalAccessTokenRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| user_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| token_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RemovePersonalAccessTokenResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### RemoveProjectGrantMemberRequest
|
### RemoveProjectGrantMemberRequest
|
||||||
|
|
||||||
|
|
||||||
|
@ -213,6 +213,20 @@ this query is always equals
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PersonalAccessToken
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| id | string | - | |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
| expiration_date | google.protobuf.Timestamp | - | |
|
||||||
|
| scopes | repeated string | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Phone
|
### Phone
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,9 @@ package management
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
@ -11,7 +13,9 @@ import (
|
|||||||
idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp"
|
idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp"
|
||||||
"github.com/caos/zitadel/internal/api/grpc/metadata"
|
"github.com/caos/zitadel/internal/api/grpc/metadata"
|
||||||
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
|
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
|
||||||
|
"github.com/caos/zitadel/internal/api/grpc/user"
|
||||||
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
|
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
|
||||||
|
z_oidc "github.com/caos/zitadel/internal/api/oidc"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
"github.com/caos/zitadel/internal/query"
|
"github.com/caos/zitadel/internal/query"
|
||||||
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
|
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
|
||||||
@ -693,6 +697,74 @@ func (s *Server) RemoveMachineKey(ctx context.Context, req *mgmt_pb.RemoveMachin
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.GetPersonalAccessTokenByIDsRequest) (*mgmt_pb.GetPersonalAccessTokenByIDsResponse, error) {
|
||||||
|
resourceOwner, err := query.NewPersonalAccessTokenResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
aggregateID, err := query.NewPersonalAccessTokenUserIDSearchQuery(req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token, err := s.query.PersonalAccessTokenByID(ctx, req.TokenId, resourceOwner, aggregateID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mgmt_pb.GetPersonalAccessTokenByIDsResponse{
|
||||||
|
Token: user.PersonalAccessTokenToPb(token),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.ListPersonalAccessTokensRequest) (*mgmt_pb.ListPersonalAccessTokensResponse, error) {
|
||||||
|
queries, err := ListPersonalAccessTokensRequestToQuery(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result, err := s.query.SearchPersonalAccessTokens(ctx, queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mgmt_pb.ListPersonalAccessTokensResponse{
|
||||||
|
Result: user_grpc.PersonalAccessTokensToPb(result.PersonalAccessTokens),
|
||||||
|
Details: obj_grpc.ToListDetails(
|
||||||
|
result.Count,
|
||||||
|
result.Sequence,
|
||||||
|
result.Timestamp,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) AddPersonalAccessToken(ctx context.Context, req *mgmt_pb.AddPersonalAccessTokenRequest) (*mgmt_pb.AddPersonalAccessTokenResponse, error) {
|
||||||
|
expDate := time.Time{}
|
||||||
|
if req.ExpirationDate != nil {
|
||||||
|
expDate = req.ExpirationDate.AsTime()
|
||||||
|
}
|
||||||
|
scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
|
||||||
|
pat, token, err := s.command.AddPersonalAccessToken(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, expDate, scopes, domain.UserTypeMachine)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mgmt_pb.AddPersonalAccessTokenResponse{
|
||||||
|
TokenId: pat.TokenID,
|
||||||
|
Token: token,
|
||||||
|
Details: obj_grpc.AddToDetailsPb(
|
||||||
|
pat.Sequence,
|
||||||
|
pat.ChangeDate,
|
||||||
|
pat.ResourceOwner,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *mgmt_pb.RemovePersonalAccessTokenRequest) (*mgmt_pb.RemovePersonalAccessTokenResponse, error) {
|
||||||
|
objectDetails, err := s.command.RemovePersonalAccessToken(ctx, req.UserId, req.TokenId, authz.GetCtxData(ctx).OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mgmt_pb.RemovePersonalAccessTokenResponse{
|
||||||
|
Details: obj_grpc.DomainToChangeDetailsPb(objectDetails),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHumanLinkedIDPsRequest) (*mgmt_pb.ListHumanLinkedIDPsResponse, error) {
|
func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHumanLinkedIDPsRequest) (*mgmt_pb.ListHumanLinkedIDPsResponse, error) {
|
||||||
queries, err := ListHumanLinkedIDPsRequestToQuery(ctx, req)
|
queries, err := ListHumanLinkedIDPsRequestToQuery(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -227,6 +227,30 @@ func AddMachineKeyRequestToDomain(req *mgmt_pb.AddMachineKeyRequest) *domain.Mac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListPersonalAccessTokensRequestToQuery(ctx context.Context, req *mgmt_pb.ListPersonalAccessTokensRequest) (*query.PersonalAccessTokenSearchQueries, error) {
|
||||||
|
resourceOwner, err := query.NewPersonalAccessTokenResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userID, err := query.NewPersonalAccessTokenUserIDSearchQuery(req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset, limit, asc := object.ListQueryToModel(req.Query)
|
||||||
|
return &query.PersonalAccessTokenSearchQueries{
|
||||||
|
SearchRequest: query.SearchRequest{
|
||||||
|
Offset: offset,
|
||||||
|
Limit: limit,
|
||||||
|
Asc: asc,
|
||||||
|
},
|
||||||
|
Queries: []query.SearchQuery{
|
||||||
|
resourceOwner,
|
||||||
|
userID,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func RemoveHumanLinkedIDPRequestToDomain(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) *domain.UserIDPLink {
|
func RemoveHumanLinkedIDPRequestToDomain(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) *domain.UserIDPLink {
|
||||||
return &domain.UserIDPLink{
|
return &domain.UserIDPLink{
|
||||||
ObjectRoot: models.ObjectRoot{
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
25
internal/api/grpc/user/machine_token.go
Normal file
25
internal/api/grpc/user/machine_token.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/api/grpc/object"
|
||||||
|
"github.com/caos/zitadel/internal/query"
|
||||||
|
"github.com/caos/zitadel/pkg/grpc/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PersonalAccessTokensToPb(tokens []*query.PersonalAccessToken) []*user.PersonalAccessToken {
|
||||||
|
t := make([]*user.PersonalAccessToken, len(tokens))
|
||||||
|
for i, token := range tokens {
|
||||||
|
t[i] = PersonalAccessTokenToPb(token)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
func PersonalAccessTokenToPb(token *query.PersonalAccessToken) *user.PersonalAccessToken {
|
||||||
|
return &user.PersonalAccessToken{
|
||||||
|
Id: token.ID,
|
||||||
|
Details: object.ChangeToDetailsPb(token.Sequence, token.ChangeDate, token.ResourceOwner),
|
||||||
|
ExpirationDate: timestamppb.New(token.Expiration),
|
||||||
|
Scopes: token.Scopes,
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
"github.com/caos/zitadel/internal/query"
|
"github.com/caos/zitadel/internal/query"
|
||||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||||
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
grant_model "github.com/caos/zitadel/internal/usergrant/model"
|
grant_model "github.com/caos/zitadel/internal/usergrant/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -227,3 +228,23 @@ func (o *OPStorage) assertProjectRoleScopes(ctx context.Context, clientID string
|
|||||||
}
|
}
|
||||||
return scopes, nil
|
return scopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.TokenView, clientID string) error {
|
||||||
|
token.Audience = append(token.Audience, clientID)
|
||||||
|
projectID, err := o.query.ProjectIDFromClientID(ctx, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ThrowPreconditionFailed(nil, "OIDC-AEG4d", "Errors.Internal")
|
||||||
|
}
|
||||||
|
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
||||||
|
}
|
||||||
|
roles, err := o.query.SearchProjectRoles(context.TODO(), &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, role := range roles.ProjectRoles {
|
||||||
|
token.Scopes = append(token.Scopes, ScopeProjectRolePrefix+role.Key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -163,6 +163,12 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
||||||
}
|
}
|
||||||
|
if token.IsPAT {
|
||||||
|
err = o.assertClientScopesForPAT(ctx, token, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, aud := range token.Audience {
|
for _, aud := range token.Audience {
|
||||||
if aud == clientID || aud == projectID {
|
if aud == clientID || aud == projectID {
|
||||||
err := o.setUserinfo(ctx, introspection, token.UserID, clientID, token.Scopes)
|
err := o.setUserinfo(ctx, introspection, token.UserID, clientID, token.Scopes)
|
||||||
|
@ -82,7 +82,8 @@ func (t *Token) EventQuery() (*es_models.SearchQuery, error) {
|
|||||||
|
|
||||||
func (t *Token) Reduce(event *es_models.Event) (err error) {
|
func (t *Token) Reduce(event *es_models.Event) (err error) {
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case user_es_model.UserTokenAdded:
|
case user_es_model.UserTokenAdded,
|
||||||
|
es_models.EventType(user_repo.PersonalAccessTokenAddedType):
|
||||||
token := new(view_model.TokenView)
|
token := new(view_model.TokenView)
|
||||||
err := token.AppendEvent(event)
|
err := token.AppendEvent(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -112,7 +113,8 @@ func (t *Token) Reduce(event *es_models.Event) (err error) {
|
|||||||
user_es_model.UserDeactivated,
|
user_es_model.UserDeactivated,
|
||||||
user_es_model.UserRemoved:
|
user_es_model.UserRemoved:
|
||||||
return t.view.DeleteUserTokens(event.AggregateID, event)
|
return t.view.DeleteUserTokens(event.AggregateID, event)
|
||||||
case es_models.EventType(user_repo.UserTokenRemovedType):
|
case es_models.EventType(user_repo.UserTokenRemovedType),
|
||||||
|
es_models.EventType(user_repo.PersonalAccessTokenRemovedType):
|
||||||
id, err := tokenIDFromRemovedEvent(event)
|
id, err := tokenIDFromRemovedEvent(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -86,7 +86,9 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
|
|||||||
if !token.Expiration.After(time.Now().UTC()) {
|
if !token.Expiration.After(time.Now().UTC()) {
|
||||||
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(err, "APP-k9KS0", "invalid token")
|
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(err, "APP-k9KS0", "invalid token")
|
||||||
}
|
}
|
||||||
|
if token.IsPAT {
|
||||||
|
return token.UserID, "", "", "", token.ResourceOwner, nil
|
||||||
|
}
|
||||||
for _, aud := range token.Audience {
|
for _, aud := range token.Audience {
|
||||||
if verifierClientID == aud || projectID == aud {
|
if verifierClientID == aud || projectID == aud {
|
||||||
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
|
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
"github.com/caos/zitadel/internal/repository/user"
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
)
|
)
|
||||||
@ -97,6 +100,18 @@ func keyWriteModelToMachineKey(wm *MachineKeyWriteModel) *domain.MachineKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func personalTokenWriteModelToToken(wm *PersonalAccessTokenWriteModel, algorithm crypto.EncryptionAlgorithm) (*domain.Token, string, error) {
|
||||||
|
encrypted, err := algorithm.Encrypt([]byte(wm.TokenID + ":" + wm.AggregateID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return &domain.Token{
|
||||||
|
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
|
||||||
|
TokenID: wm.TokenID,
|
||||||
|
Expiration: wm.ExpirationDate,
|
||||||
|
}, base64.RawURLEncoding.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
func readModelToU2FTokens(wm *HumanU2FTokensReadModel) []*domain.WebAuthNToken {
|
func readModelToU2FTokens(wm *HumanU2FTokensReadModel) []*domain.WebAuthNToken {
|
||||||
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
|
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
|
||||||
for i, token := range wm.WebAuthNTokens {
|
for i, token := range wm.WebAuthNTokens {
|
||||||
|
@ -16,6 +16,7 @@ type UserWriteModel struct {
|
|||||||
UserName string
|
UserName string
|
||||||
IDPLinks []*domain.UserIDPLink
|
IDPLinks []*domain.UserIDPLink
|
||||||
UserState domain.UserState
|
UserState domain.UserState
|
||||||
|
UserType domain.UserType
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserWriteModel(userID, resourceOwner string) *UserWriteModel {
|
func NewUserWriteModel(userID, resourceOwner string) *UserWriteModel {
|
||||||
@ -34,9 +35,11 @@ func (wm *UserWriteModel) Reduce() error {
|
|||||||
case *user.HumanAddedEvent:
|
case *user.HumanAddedEvent:
|
||||||
wm.UserName = e.UserName
|
wm.UserName = e.UserName
|
||||||
wm.UserState = domain.UserStateActive
|
wm.UserState = domain.UserStateActive
|
||||||
|
wm.UserType = domain.UserTypeHuman
|
||||||
case *user.HumanRegisteredEvent:
|
case *user.HumanRegisteredEvent:
|
||||||
wm.UserName = e.UserName
|
wm.UserName = e.UserName
|
||||||
wm.UserState = domain.UserStateActive
|
wm.UserState = domain.UserStateActive
|
||||||
|
wm.UserType = domain.UserTypeHuman
|
||||||
case *user.HumanInitialCodeAddedEvent:
|
case *user.HumanInitialCodeAddedEvent:
|
||||||
wm.UserState = domain.UserStateInitial
|
wm.UserState = domain.UserStateInitial
|
||||||
case *user.HumanInitializedCheckSucceededEvent:
|
case *user.HumanInitializedCheckSucceededEvent:
|
||||||
@ -62,6 +65,7 @@ func (wm *UserWriteModel) Reduce() error {
|
|||||||
case *user.MachineAddedEvent:
|
case *user.MachineAddedEvent:
|
||||||
wm.UserName = e.UserName
|
wm.UserName = e.UserName
|
||||||
wm.UserState = domain.UserStateActive
|
wm.UserState = domain.UserStateActive
|
||||||
|
wm.UserType = domain.UserTypeMachine
|
||||||
case *user.UsernameChangedEvent:
|
case *user.UsernameChangedEvent:
|
||||||
wm.UserName = e.UserName
|
wm.UserName = e.UserName
|
||||||
case *user.UserLockedEvent:
|
case *user.UserLockedEvent:
|
||||||
|
92
internal/command/user_personal_access_token.go
Normal file
92
internal/command/user_personal_access_token.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Commands) AddPersonalAccessToken(ctx context.Context, userID, resourceOwner string, expirationDate time.Time, scopes []string, allowedUserType domain.UserType) (*domain.Token, string, error) {
|
||||||
|
userWriteModel, err := c.userWriteModelByID(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if !isUserStateExists(userWriteModel.UserState) {
|
||||||
|
return nil, "", errors.ThrowPreconditionFailed(nil, "COMMAND-Dggw2", "Errors.User.NotFound")
|
||||||
|
}
|
||||||
|
if allowedUserType != domain.UserTypeUnspecified && userWriteModel.UserType != allowedUserType {
|
||||||
|
return nil, "", errors.ThrowPreconditionFailed(nil, "COMMAND-Df2f1", "Errors.User.WrongType")
|
||||||
|
}
|
||||||
|
tokenID, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
tokenWriteModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner)
|
||||||
|
err = c.eventstore.FilterToQueryReducer(ctx, tokenWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationDate, err = domain.ValidateExpirationDate(expirationDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := c.eventstore.Push(ctx,
|
||||||
|
user.NewPersonalAccessTokenAddedEvent(
|
||||||
|
ctx,
|
||||||
|
UserAggregateFromWriteModel(&tokenWriteModel.WriteModel),
|
||||||
|
tokenID,
|
||||||
|
expirationDate,
|
||||||
|
scopes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(tokenWriteModel, events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return personalTokenWriteModelToToken(tokenWriteModel, c.keyAlgorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) RemovePersonalAccessToken(ctx context.Context, userID, tokenID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||||
|
tokenWriteModel, err := c.personalAccessTokenWriteModelByID(ctx, userID, tokenID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !tokenWriteModel.Exists() {
|
||||||
|
return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.PAT.NotFound")
|
||||||
|
}
|
||||||
|
|
||||||
|
pushedEvents, err := c.eventstore.Push(ctx,
|
||||||
|
user.NewPersonalAccessTokenRemovedEvent(ctx, UserAggregateFromWriteModel(&tokenWriteModel.WriteModel), tokenID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(tokenWriteModel, pushedEvents...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&tokenWriteModel.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) personalAccessTokenWriteModelByID(ctx context.Context, userID, tokenID, resourceOwner string) (writeModel *PersonalAccessTokenWriteModel, err error) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4n8vs", "Errors.User.UserIDMissing")
|
||||||
|
}
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
writeModel = NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner)
|
||||||
|
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModel, nil
|
||||||
|
}
|
81
internal/command/user_personal_access_token_model.go
Normal file
81
internal/command/user_personal_access_token_model.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PersonalAccessTokenWriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
|
||||||
|
TokenID string
|
||||||
|
ExpirationDate time.Time
|
||||||
|
|
||||||
|
State domain.PersonalAccessTokenState
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner string) *PersonalAccessTokenWriteModel {
|
||||||
|
return &PersonalAccessTokenWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: userID,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
TokenID: tokenID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *PersonalAccessTokenWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||||
|
for _, event := range events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *user.PersonalAccessTokenAddedEvent:
|
||||||
|
if wm.TokenID != e.TokenID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *user.PersonalAccessTokenRemovedEvent:
|
||||||
|
if wm.TokenID != e.TokenID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *user.UserRemovedEvent:
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *PersonalAccessTokenWriteModel) Reduce() error {
|
||||||
|
for _, event := range wm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *user.PersonalAccessTokenAddedEvent:
|
||||||
|
wm.TokenID = e.TokenID
|
||||||
|
wm.ExpirationDate = e.Expiration
|
||||||
|
wm.State = domain.PersonalAccessTokenStateActive
|
||||||
|
case *user.PersonalAccessTokenRemovedEvent:
|
||||||
|
wm.State = domain.PersonalAccessTokenStateRemoved
|
||||||
|
case *user.UserRemovedEvent:
|
||||||
|
wm.State = domain.PersonalAccessTokenStateRemoved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wm.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *PersonalAccessTokenWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
ResourceOwner(wm.ResourceOwner).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(user.AggregateType).
|
||||||
|
AggregateIDs(wm.AggregateID).
|
||||||
|
EventTypes(
|
||||||
|
user.PersonalAccessTokenAddedType,
|
||||||
|
user.PersonalAccessTokenRemovedType,
|
||||||
|
user.UserRemovedType).
|
||||||
|
Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *PersonalAccessTokenWriteModel) Exists() bool {
|
||||||
|
return wm.State != domain.PersonalAccessTokenStateUnspecified && wm.State != domain.PersonalAccessTokenStateRemoved
|
||||||
|
}
|
297
internal/command/user_personal_access_token_test.go
Normal file
297
internal/command/user_personal_access_token_test.go
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
|
|
||||||
|
id_mock "github.com/caos/zitadel/internal/id/mock"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
|
||||||
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
"github.com/caos/zitadel/internal/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommands_AddPersonalAccessToken(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
idGenerator id.Generator
|
||||||
|
keyAlgorithm crypto.EncryptionAlgorithm
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
userID string
|
||||||
|
resourceOwner string
|
||||||
|
expirationDate time.Time
|
||||||
|
scopes []string
|
||||||
|
allowedUserType domain.UserType
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want *domain.Token
|
||||||
|
token string
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"user does not exist, error",
|
||||||
|
fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
scopes: []string{"openid"},
|
||||||
|
expirationDate: time.Time{},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: caos_errs.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user type not allowed, error",
|
||||||
|
fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewMachineAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"machine",
|
||||||
|
"Machine",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
expirationDate: time.Time{},
|
||||||
|
scopes: []string{"openid"},
|
||||||
|
allowedUserType: domain.UserTypeHuman,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: caos_errs.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid expiration date, error",
|
||||||
|
fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewMachineAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"machine",
|
||||||
|
"Machine",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
expirationDate: time.Now().Add(-24 * time.Hour),
|
||||||
|
scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token added",
|
||||||
|
fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewMachineAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"machine",
|
||||||
|
"Machine",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewPersonalAccessTokenAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"token1",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
[]string{"openid"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"),
|
||||||
|
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
expirationDate: time.Time{},
|
||||||
|
scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
want: &domain.Token{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
TokenID: "token1",
|
||||||
|
Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
},
|
||||||
|
token: base64.RawURLEncoding.EncodeToString([]byte("token1:user1")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
keyAlgorithm: tt.fields.keyAlgorithm,
|
||||||
|
}
|
||||||
|
got, token, err := c.AddPersonalAccessToken(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.expirationDate, tt.args.scopes, tt.args.allowedUserType)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
assert.Equal(t, tt.res.token, token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_RemovePersonalAccessToken(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
userID string
|
||||||
|
tokenID string
|
||||||
|
resourceOwner string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want *domain.ObjectDetails
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"token does not exist, error",
|
||||||
|
fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
userID: "user1",
|
||||||
|
tokenID: "token1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: caos_errs.IsNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remove token, ok",
|
||||||
|
fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewPersonalAccessTokenAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"token1",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
[]string{"openid"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewPersonalAccessTokenRemovedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"token1",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
userID: "user1",
|
||||||
|
tokenID: "token1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
}
|
||||||
|
got, err := c.RemovePersonalAccessToken(tt.args.ctx, tt.args.userID, tt.args.tokenID, tt.args.resourceOwner)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,16 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/crypto"
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
//most of us won't survive until 12-31-9999 23:59:59, maybe ZITADEL does
|
|
||||||
defaultExpDate = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthNKey interface {
|
|
||||||
}
|
|
||||||
|
|
||||||
type authNKey interface {
|
type authNKey interface {
|
||||||
setPublicKey([]byte)
|
setPublicKey([]byte)
|
||||||
setPrivateKey([]byte)
|
setPrivateKey([]byte)
|
||||||
expirationDate() time.Time
|
expiration
|
||||||
setExpirationDate(time.Time)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthNKeyType int32
|
type AuthNKeyType int32
|
||||||
@ -50,16 +39,6 @@ func (key *MachineKey) GenerateNewMachineKeyPair(keySize int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureValidExpirationDate(key authNKey) error {
|
|
||||||
if key.expirationDate().IsZero() {
|
|
||||||
key.setExpirationDate(defaultExpDate)
|
|
||||||
}
|
|
||||||
if key.expirationDate().Before(time.Now()) {
|
|
||||||
return errors.ThrowInvalidArgument(nil, "AUTHN-dv3t5", "Errors.AuthNKey.ExpireBeforeNow")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetNewAuthNKeyPair(key authNKey, keySize int) error {
|
func SetNewAuthNKeyPair(key authNKey, keySize int) error {
|
||||||
privateKey, publicKey, err := NewAuthNKeyPair(keySize)
|
privateKey, publicKey, err := NewAuthNKeyPair(keySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
36
internal/domain/expiration.go
Normal file
36
internal/domain/expiration.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//most of us won't survive until 12-31-9999 23:59:59, maybe ZITADEL does
|
||||||
|
defaultExpDate = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
type expiration interface {
|
||||||
|
expirationDate() time.Time
|
||||||
|
setExpirationDate(time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureValidExpirationDate(key expiration) error {
|
||||||
|
date, err := ValidateExpirationDate(key.expirationDate())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key.setExpirationDate(date)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateExpirationDate(date time.Time) (time.Time, error) {
|
||||||
|
if date.IsZero() {
|
||||||
|
return defaultExpDate, nil
|
||||||
|
}
|
||||||
|
if date.Before(time.Now()) {
|
||||||
|
return time.Time{}, errors.ThrowInvalidArgument(nil, "DOMAIN-dv3t5", "Errors.AuthNKey.ExpireBeforeNow")
|
||||||
|
}
|
||||||
|
return date, nil
|
||||||
|
}
|
@ -53,3 +53,17 @@ const (
|
|||||||
func (f UserAuthMethodType) Valid() bool {
|
func (f UserAuthMethodType) Valid() bool {
|
||||||
return f >= 0 && f < userAuthMethodTypeCount
|
return f >= 0 && f < userAuthMethodTypeCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PersonalAccessTokenState int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
PersonalAccessTokenStateUnspecified PersonalAccessTokenState = iota
|
||||||
|
PersonalAccessTokenStateActive
|
||||||
|
PersonalAccessTokenStateRemoved
|
||||||
|
|
||||||
|
personalAccessTokenStateCount
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f PersonalAccessTokenState) Valid() bool {
|
||||||
|
return f >= 0 && f < personalAccessTokenStateCount
|
||||||
|
}
|
||||||
|
@ -64,6 +64,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co
|
|||||||
NewProjectMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_members"]))
|
NewProjectMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_members"]))
|
||||||
NewProjectGrantMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_members"]))
|
NewProjectGrantMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_members"]))
|
||||||
NewAuthNKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authn_keys"]))
|
NewAuthNKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authn_keys"]))
|
||||||
|
NewPersonalAccessTokenProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["personal_access_tokens"]))
|
||||||
NewUserGrantProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_grants"]))
|
NewUserGrantProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_grants"]))
|
||||||
NewUserMetadataProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_metadata"]))
|
NewUserMetadataProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_metadata"]))
|
||||||
NewUserAuthMethodProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_auth_method"]))
|
NewUserAuthMethodProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_auth_method"]))
|
||||||
|
112
internal/query/projection/user_personal_access_token.go
Normal file
112
internal/query/projection/user_personal_access_token.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/caos/logging"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/handler"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PersonalAccessTokenProjection struct {
|
||||||
|
crdb.StatementHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PersonalAccessTokenProjectionTable = "zitadel.projections.personal_access_tokens"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPersonalAccessTokenProjection(ctx context.Context, config crdb.StatementHandlerConfig) *PersonalAccessTokenProjection {
|
||||||
|
p := &PersonalAccessTokenProjection{}
|
||||||
|
config.ProjectionName = PersonalAccessTokenProjectionTable
|
||||||
|
config.Reducers = p.reducers()
|
||||||
|
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PersonalAccessTokenProjection) reducers() []handler.AggregateReducer {
|
||||||
|
return []handler.AggregateReducer{
|
||||||
|
{
|
||||||
|
Aggregate: user.AggregateType,
|
||||||
|
EventRedusers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: user.PersonalAccessTokenAddedType,
|
||||||
|
Reduce: p.reducePersonalAccessTokenAdded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Event: user.PersonalAccessTokenRemovedType,
|
||||||
|
Reduce: p.reducePersonalAccessTokenRemoved,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Event: user.UserRemovedType,
|
||||||
|
Reduce: p.reduceUserRemoved,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PersonalAccessTokenColumnID = "id"
|
||||||
|
PersonalAccessTokenColumnCreationDate = "creation_date"
|
||||||
|
PersonalAccessTokenColumnChangeDate = "change_date"
|
||||||
|
PersonalAccessTokenColumnResourceOwner = "resource_owner"
|
||||||
|
PersonalAccessTokenColumnSequence = "sequence"
|
||||||
|
PersonalAccessTokenColumnUserID = "user_id"
|
||||||
|
PersonalAccessTokenColumnExpiration = "expiration"
|
||||||
|
PersonalAccessTokenColumnScopes = "scopes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *PersonalAccessTokenProjection) reducePersonalAccessTokenAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, ok := event.(*user.PersonalAccessTokenAddedEvent)
|
||||||
|
if !ok {
|
||||||
|
logging.LogWithFields("HANDL-Dbfg2", "seq", event.Sequence(), "expectedType", user.PersonalAccessTokenAddedType).Error("wrong event type")
|
||||||
|
return nil, errors.ThrowInvalidArgument(nil, "HANDL-DVgf7", "reduce.wrong.event.type")
|
||||||
|
}
|
||||||
|
return crdb.NewCreateStatement(
|
||||||
|
e,
|
||||||
|
[]handler.Column{
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnID, e.TokenID),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnCreationDate, e.CreationDate()),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnChangeDate, e.CreationDate()),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnSequence, e.Sequence()),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnUserID, e.Aggregate().ID),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnExpiration, e.Expiration),
|
||||||
|
handler.NewCol(PersonalAccessTokenColumnScopes, pq.StringArray(e.Scopes)),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PersonalAccessTokenProjection) reducePersonalAccessTokenRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, ok := event.(*user.PersonalAccessTokenRemovedEvent)
|
||||||
|
if !ok {
|
||||||
|
logging.LogWithFields("HANDL-Edf32", "seq", event.Sequence(), "expectedType", user.PersonalAccessTokenRemovedType).Error("wrong event type")
|
||||||
|
return nil, errors.ThrowInvalidArgument(nil, "HANDL-g7u3F", "reduce.wrong.event.type")
|
||||||
|
}
|
||||||
|
return crdb.NewDeleteStatement(
|
||||||
|
e,
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(PersonalAccessTokenColumnID, e.TokenID),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PersonalAccessTokenProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, ok := event.(*user.UserRemovedEvent)
|
||||||
|
if !ok {
|
||||||
|
logging.LogWithFields("HANDL-GEg43", "seq", event.Sequence(), "expectedType", user.UserRemovedType).Error("wrong event type")
|
||||||
|
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Dff3h", "reduce.wrong.event.type")
|
||||||
|
}
|
||||||
|
return crdb.NewDeleteStatement(
|
||||||
|
e,
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(PersonalAccessTokenColumnUserID, e.Aggregate().ID),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
128
internal/query/projection/user_personal_access_token_test.go
Normal file
128
internal/query/projection/user_personal_access_token_test.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/handler"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPersonalAccessTokenProjection_reduces(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
event func(t *testing.T) eventstore.Event
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||||
|
want wantReduce
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reducePersonalAccessTokenAdded",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
repository.EventType(user.PersonalAccessTokenAddedType),
|
||||||
|
user.AggregateType,
|
||||||
|
[]byte(`{"tokenId": "tokenID", "expiration": "9999-12-31T23:59:59Z", "scopes": ["openid"]}`),
|
||||||
|
), user.PersonalAccessTokenAddedEventMapper),
|
||||||
|
},
|
||||||
|
reduce: (&PersonalAccessTokenProjection{}).reducePersonalAccessTokenAdded,
|
||||||
|
want: wantReduce{
|
||||||
|
projection: PersonalAccessTokenProjectionTable,
|
||||||
|
aggregateType: eventstore.AggregateType("user"),
|
||||||
|
sequence: 15,
|
||||||
|
previousSequence: 10,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO zitadel.projections.personal_access_tokens (id, creation_date, change_date, resource_owner, sequence, user_id, expiration, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"tokenID",
|
||||||
|
anyArg{},
|
||||||
|
anyArg{},
|
||||||
|
"ro-id",
|
||||||
|
uint64(15),
|
||||||
|
"agg-id",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
pq.StringArray{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reducePersonalAccessTokenRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
repository.EventType(user.PersonalAccessTokenRemovedType),
|
||||||
|
user.AggregateType,
|
||||||
|
[]byte(`{"tokenId": "tokenID"}`),
|
||||||
|
), user.PersonalAccessTokenRemovedEventMapper),
|
||||||
|
},
|
||||||
|
reduce: (&PersonalAccessTokenProjection{}).reducePersonalAccessTokenRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
projection: PersonalAccessTokenProjectionTable,
|
||||||
|
aggregateType: eventstore.AggregateType("user"),
|
||||||
|
sequence: 15,
|
||||||
|
previousSequence: 10,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM zitadel.projections.personal_access_tokens WHERE (id = $1)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"tokenID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceUserRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
repository.EventType(user.PersonalAccessTokenRemovedType),
|
||||||
|
user.AggregateType,
|
||||||
|
nil,
|
||||||
|
), user.UserRemovedEventMapper),
|
||||||
|
},
|
||||||
|
reduce: (&PersonalAccessTokenProjection{}).reduceUserRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
projection: PersonalAccessTokenProjectionTable,
|
||||||
|
aggregateType: eventstore.AggregateType("user"),
|
||||||
|
sequence: 15,
|
||||||
|
previousSequence: 10,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM zitadel.projections.personal_access_tokens WHERE (user_id = $1)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := baseEvent(t)
|
||||||
|
got, err := tt.reduce(event)
|
||||||
|
if _, ok := err.(errors.InvalidArgument); !ok {
|
||||||
|
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
event = tt.args.event(t)
|
||||||
|
got, err = tt.reduce(event)
|
||||||
|
assertReduce(t, got, err, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
219
internal/query/user_personal_access_token.go
Normal file
219
internal/query/user_personal_access_token.go
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
errs "errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/query/projection"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
personalAccessTokensTable = table{
|
||||||
|
name: projection.PersonalAccessTokenProjectionTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnID = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnID,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnUserID = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnUserID,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnExpiration = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnExpiration,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnScopes = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnScopes,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnCreationDate = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnCreationDate,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnChangeDate = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnChangeDate,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnResourceOwner = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnResourceOwner,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
PersonalAccessTokenColumnSequence = Column{
|
||||||
|
name: projection.PersonalAccessTokenColumnSequence,
|
||||||
|
table: personalAccessTokensTable,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type PersonalAccessTokens struct {
|
||||||
|
SearchResponse
|
||||||
|
PersonalAccessTokens []*PersonalAccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalAccessToken struct {
|
||||||
|
ID string
|
||||||
|
CreationDate time.Time
|
||||||
|
ChangeDate time.Time
|
||||||
|
ResourceOwner string
|
||||||
|
Sequence uint64
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
Expiration time.Time
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalAccessTokenSearchQueries struct {
|
||||||
|
SearchRequest
|
||||||
|
Queries []SearchQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) PersonalAccessTokenByID(ctx context.Context, id string, queries ...SearchQuery) (*PersonalAccessToken, error) {
|
||||||
|
query, scan := preparePersonalAccessTokenQuery()
|
||||||
|
for _, q := range queries {
|
||||||
|
query = q.toQuery(query)
|
||||||
|
}
|
||||||
|
stmt, args, err := query.Where(sq.Eq{
|
||||||
|
PersonalAccessTokenColumnID.identifier(): id,
|
||||||
|
}).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatment")
|
||||||
|
}
|
||||||
|
|
||||||
|
row := q.client.QueryRowContext(ctx, stmt, args...)
|
||||||
|
return scan(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries) (personalAccessTokens *PersonalAccessTokens, err error) {
|
||||||
|
query, scan := preparePersonalAccessTokensQuery()
|
||||||
|
stmt, args, err := queries.toQuery(query).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInvalidArgument(err, "QUERY-Hjw2w", "Errors.Query.InvalidRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := q.client.QueryContext(ctx, stmt, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "QUERY-Bmz63", "Errors.Internal")
|
||||||
|
}
|
||||||
|
personalAccessTokens, err = scan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
personalAccessTokens.LatestSequence, err = q.latestSequence(ctx, personalAccessTokensTable)
|
||||||
|
return personalAccessTokens, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPersonalAccessTokenResourceOwnerSearchQuery(value string) (SearchQuery, error) {
|
||||||
|
return NewTextQuery(PersonalAccessTokenColumnResourceOwner, value, TextEquals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPersonalAccessTokenUserIDSearchQuery(value string) (SearchQuery, error) {
|
||||||
|
return NewTextQuery(PersonalAccessTokenColumnUserID, value, TextEquals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PersonalAccessTokenSearchQueries) AppendMyResourceOwnerQuery(orgID string) error {
|
||||||
|
query, err := NewPersonalAccessTokenResourceOwnerSearchQuery(orgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Queries = append(r.Queries, query)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *PersonalAccessTokenSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
||||||
|
query = q.SearchRequest.toQuery(query)
|
||||||
|
for _, q := range q.Queries {
|
||||||
|
query = q.toQuery(query)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePersonalAccessTokenQuery() (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) {
|
||||||
|
return sq.Select(
|
||||||
|
PersonalAccessTokenColumnID.identifier(),
|
||||||
|
PersonalAccessTokenColumnCreationDate.identifier(),
|
||||||
|
PersonalAccessTokenColumnChangeDate.identifier(),
|
||||||
|
PersonalAccessTokenColumnResourceOwner.identifier(),
|
||||||
|
PersonalAccessTokenColumnSequence.identifier(),
|
||||||
|
PersonalAccessTokenColumnUserID.identifier(),
|
||||||
|
PersonalAccessTokenColumnExpiration.identifier(),
|
||||||
|
PersonalAccessTokenColumnScopes.identifier()).
|
||||||
|
From(personalAccessTokensTable.identifier()).PlaceholderFormat(sq.Dollar),
|
||||||
|
func(row *sql.Row) (*PersonalAccessToken, error) {
|
||||||
|
p := new(PersonalAccessToken)
|
||||||
|
scopes := pq.StringArray{}
|
||||||
|
err := row.Scan(
|
||||||
|
&p.ID,
|
||||||
|
&p.CreationDate,
|
||||||
|
&p.ChangeDate,
|
||||||
|
&p.ResourceOwner,
|
||||||
|
&p.Sequence,
|
||||||
|
&p.UserID,
|
||||||
|
&p.Expiration,
|
||||||
|
&scopes,
|
||||||
|
)
|
||||||
|
p.Scopes = scopes
|
||||||
|
if err != nil {
|
||||||
|
if errs.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, errors.ThrowNotFound(err, "QUERY-fk2fs", "Errors.PersonalAccessToken.NotFound")
|
||||||
|
}
|
||||||
|
return nil, errors.ThrowInternal(err, "QUERY-dj2FF", "Errors.Internal")
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePersonalAccessTokensQuery() (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) {
|
||||||
|
return sq.Select(
|
||||||
|
PersonalAccessTokenColumnID.identifier(),
|
||||||
|
PersonalAccessTokenColumnCreationDate.identifier(),
|
||||||
|
PersonalAccessTokenColumnChangeDate.identifier(),
|
||||||
|
PersonalAccessTokenColumnResourceOwner.identifier(),
|
||||||
|
PersonalAccessTokenColumnSequence.identifier(),
|
||||||
|
PersonalAccessTokenColumnUserID.identifier(),
|
||||||
|
PersonalAccessTokenColumnExpiration.identifier(),
|
||||||
|
PersonalAccessTokenColumnScopes.identifier(),
|
||||||
|
countColumn.identifier()).
|
||||||
|
From(personalAccessTokensTable.identifier()).PlaceholderFormat(sq.Dollar),
|
||||||
|
func(rows *sql.Rows) (*PersonalAccessTokens, error) {
|
||||||
|
personalAccessTokens := make([]*PersonalAccessToken, 0)
|
||||||
|
var count uint64
|
||||||
|
for rows.Next() {
|
||||||
|
token := new(PersonalAccessToken)
|
||||||
|
scopes := pq.StringArray{}
|
||||||
|
err := rows.Scan(
|
||||||
|
&token.ID,
|
||||||
|
&token.CreationDate,
|
||||||
|
&token.ChangeDate,
|
||||||
|
&token.ResourceOwner,
|
||||||
|
&token.Sequence,
|
||||||
|
&token.UserID,
|
||||||
|
&token.Expiration,
|
||||||
|
&scopes,
|
||||||
|
&count,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token.Scopes = scopes
|
||||||
|
personalAccessTokens = append(personalAccessTokens, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "QUERY-QMXJv", "Errors.Query.CloseRows")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PersonalAccessTokens{
|
||||||
|
PersonalAccessTokens: personalAccessTokens,
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: count,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
271
internal/query/user_personal_access_token_test.go
Normal file
271
internal/query/user_personal_access_token_test.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
errs "github.com/caos/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
personalAccessTokenStmt = regexp.QuoteMeta(
|
||||||
|
"SELECT zitadel.projections.personal_access_tokens.id," +
|
||||||
|
" zitadel.projections.personal_access_tokens.creation_date," +
|
||||||
|
" zitadel.projections.personal_access_tokens.change_date," +
|
||||||
|
" zitadel.projections.personal_access_tokens.resource_owner," +
|
||||||
|
" zitadel.projections.personal_access_tokens.sequence," +
|
||||||
|
" zitadel.projections.personal_access_tokens.user_id," +
|
||||||
|
" zitadel.projections.personal_access_tokens.expiration," +
|
||||||
|
" zitadel.projections.personal_access_tokens.scopes" +
|
||||||
|
" FROM zitadel.projections.personal_access_tokens")
|
||||||
|
personalAccessTokenCols = []string{
|
||||||
|
"id",
|
||||||
|
"creation_date",
|
||||||
|
"change_date",
|
||||||
|
"resource_owner",
|
||||||
|
"sequence",
|
||||||
|
"user_id",
|
||||||
|
"expiration",
|
||||||
|
"scopes",
|
||||||
|
}
|
||||||
|
personalAccessTokensStmt = regexp.QuoteMeta(
|
||||||
|
"SELECT zitadel.projections.personal_access_tokens.id," +
|
||||||
|
" zitadel.projections.personal_access_tokens.creation_date," +
|
||||||
|
" zitadel.projections.personal_access_tokens.change_date," +
|
||||||
|
" zitadel.projections.personal_access_tokens.resource_owner," +
|
||||||
|
" zitadel.projections.personal_access_tokens.sequence," +
|
||||||
|
" zitadel.projections.personal_access_tokens.user_id," +
|
||||||
|
" zitadel.projections.personal_access_tokens.expiration," +
|
||||||
|
" zitadel.projections.personal_access_tokens.scopes," +
|
||||||
|
" COUNT(*) OVER ()" +
|
||||||
|
" FROM zitadel.projections.personal_access_tokens")
|
||||||
|
personalAccessTokensCols = []string{
|
||||||
|
"id",
|
||||||
|
"creation_date",
|
||||||
|
"change_date",
|
||||||
|
"resource_owner",
|
||||||
|
"sequence",
|
||||||
|
"user_id",
|
||||||
|
"expiration",
|
||||||
|
"scopes",
|
||||||
|
"count",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_PersonalAccessTokenPrepares(t *testing.T) {
|
||||||
|
type want struct {
|
||||||
|
sqlExpectations sqlExpectation
|
||||||
|
err checkErr
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
prepare interface{}
|
||||||
|
want want
|
||||||
|
object interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokenQuery no result",
|
||||||
|
prepare: preparePersonalAccessTokenQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQuery(
|
||||||
|
personalAccessTokenStmt,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
err: func(err error) (error, bool) {
|
||||||
|
if !errs.IsNotFound(err) {
|
||||||
|
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object: (*PersonalAccessToken)(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokenQuery found",
|
||||||
|
prepare: preparePersonalAccessTokenQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQuery(
|
||||||
|
personalAccessTokenStmt,
|
||||||
|
personalAccessTokenCols,
|
||||||
|
[]driver.Value{
|
||||||
|
"token-id",
|
||||||
|
testNow,
|
||||||
|
testNow,
|
||||||
|
"ro",
|
||||||
|
uint64(20211202),
|
||||||
|
"user-id",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
pq.StringArray{"openid"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &PersonalAccessToken{
|
||||||
|
ID: "token-id",
|
||||||
|
CreationDate: testNow,
|
||||||
|
ChangeDate: testNow,
|
||||||
|
ResourceOwner: "ro",
|
||||||
|
Sequence: 20211202,
|
||||||
|
UserID: "user-id",
|
||||||
|
Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokenQuery sql err",
|
||||||
|
prepare: preparePersonalAccessTokenQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueryErr(
|
||||||
|
personalAccessTokenStmt,
|
||||||
|
sql.ErrConnDone,
|
||||||
|
),
|
||||||
|
err: func(err error) (error, bool) {
|
||||||
|
if !errors.Is(err, sql.ErrConnDone) {
|
||||||
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokensQuery no result",
|
||||||
|
prepare: preparePersonalAccessTokensQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
personalAccessTokensStmt,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &PersonalAccessTokens{PersonalAccessTokens: []*PersonalAccessToken{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokensQuery one token",
|
||||||
|
prepare: preparePersonalAccessTokensQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
personalAccessTokensStmt,
|
||||||
|
personalAccessTokensCols,
|
||||||
|
[][]driver.Value{
|
||||||
|
{
|
||||||
|
"token-id",
|
||||||
|
testNow,
|
||||||
|
testNow,
|
||||||
|
"ro",
|
||||||
|
uint64(20211202),
|
||||||
|
"user-id",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
pq.StringArray{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &PersonalAccessTokens{
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: 1,
|
||||||
|
},
|
||||||
|
PersonalAccessTokens: []*PersonalAccessToken{
|
||||||
|
{
|
||||||
|
ID: "token-id",
|
||||||
|
CreationDate: testNow,
|
||||||
|
ChangeDate: testNow,
|
||||||
|
ResourceOwner: "ro",
|
||||||
|
Sequence: 20211202,
|
||||||
|
UserID: "user-id",
|
||||||
|
Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokensQuery multiple tokens",
|
||||||
|
prepare: preparePersonalAccessTokensQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
personalAccessTokensStmt,
|
||||||
|
personalAccessTokensCols,
|
||||||
|
[][]driver.Value{
|
||||||
|
{
|
||||||
|
"token-id",
|
||||||
|
testNow,
|
||||||
|
testNow,
|
||||||
|
"ro",
|
||||||
|
uint64(20211202),
|
||||||
|
"user-id",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
pq.StringArray{"openid"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token-id2",
|
||||||
|
testNow,
|
||||||
|
testNow,
|
||||||
|
"ro",
|
||||||
|
uint64(20211202),
|
||||||
|
"user-id",
|
||||||
|
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
pq.StringArray{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &PersonalAccessTokens{
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: 2,
|
||||||
|
},
|
||||||
|
PersonalAccessTokens: []*PersonalAccessToken{
|
||||||
|
{
|
||||||
|
ID: "token-id",
|
||||||
|
CreationDate: testNow,
|
||||||
|
ChangeDate: testNow,
|
||||||
|
ResourceOwner: "ro",
|
||||||
|
Sequence: 20211202,
|
||||||
|
UserID: "user-id",
|
||||||
|
Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "token-id2",
|
||||||
|
CreationDate: testNow,
|
||||||
|
ChangeDate: testNow,
|
||||||
|
ResourceOwner: "ro",
|
||||||
|
Sequence: 20211202,
|
||||||
|
UserID: "user-id",
|
||||||
|
Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preparePersonalAccessTokensQuery sql err",
|
||||||
|
prepare: preparePersonalAccessTokensQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueryErr(
|
||||||
|
personalAccessTokensStmt,
|
||||||
|
sql.ErrConnDone,
|
||||||
|
),
|
||||||
|
err: func(err error) (error, bool) {
|
||||||
|
if !errors.Is(err, sql.ErrConnDone) {
|
||||||
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -111,5 +111,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
|||||||
RegisterFilterEventMapper(MachineAddedEventType, MachineAddedEventMapper).
|
RegisterFilterEventMapper(MachineAddedEventType, MachineAddedEventMapper).
|
||||||
RegisterFilterEventMapper(MachineChangedEventType, MachineChangedEventMapper).
|
RegisterFilterEventMapper(MachineChangedEventType, MachineChangedEventMapper).
|
||||||
RegisterFilterEventMapper(MachineKeyAddedEventType, MachineKeyAddedEventMapper).
|
RegisterFilterEventMapper(MachineKeyAddedEventType, MachineKeyAddedEventMapper).
|
||||||
RegisterFilterEventMapper(MachineKeyRemovedEventType, MachineKeyRemovedEventMapper)
|
RegisterFilterEventMapper(MachineKeyRemovedEventType, MachineKeyRemovedEventMapper).
|
||||||
|
RegisterFilterEventMapper(PersonalAccessTokenAddedType, PersonalAccessTokenAddedEventMapper).
|
||||||
|
RegisterFilterEventMapper(PersonalAccessTokenRemovedType, PersonalAccessTokenRemovedEventMapper)
|
||||||
}
|
}
|
||||||
|
105
internal/repository/user/personal_access_token.go
Normal file
105
internal/repository/user/personal_access_token.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
personalAccessTokenEventPrefix = userEventTypePrefix + "pat."
|
||||||
|
PersonalAccessTokenAddedType = personalAccessTokenEventPrefix + "added"
|
||||||
|
PersonalAccessTokenRemovedType = personalAccessTokenEventPrefix + "removed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PersonalAccessTokenAddedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
TokenID string `json:"tokenId"`
|
||||||
|
Expiration time.Time `json:"expiration"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PersonalAccessTokenAddedEvent) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PersonalAccessTokenAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPersonalAccessTokenAddedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
tokenID string,
|
||||||
|
expiration time.Time,
|
||||||
|
scopes []string,
|
||||||
|
) *PersonalAccessTokenAddedEvent {
|
||||||
|
return &PersonalAccessTokenAddedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
PersonalAccessTokenAddedType,
|
||||||
|
),
|
||||||
|
TokenID: tokenID,
|
||||||
|
Expiration: expiration,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PersonalAccessTokenAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||||
|
tokenAdded := &PersonalAccessTokenAddedEvent{
|
||||||
|
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(event.Data, tokenAdded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "USER-Dbges", "unable to unmarshal token added")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenAdded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalAccessTokenRemovedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
TokenID string `json:"tokenId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PersonalAccessTokenRemovedEvent) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PersonalAccessTokenRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPersonalAccessTokenRemovedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
tokenID string,
|
||||||
|
) *PersonalAccessTokenRemovedEvent {
|
||||||
|
return &PersonalAccessTokenRemovedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
PersonalAccessTokenRemovedType,
|
||||||
|
),
|
||||||
|
TokenID: tokenID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PersonalAccessTokenRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||||
|
tokenRemoved := &PersonalAccessTokenRemovedEvent{
|
||||||
|
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(event.Data, tokenRemoved)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "USER-Dbneg", "unable to unmarshal token removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenRemoved, nil
|
||||||
|
}
|
@ -66,8 +66,11 @@ Errors:
|
|||||||
Machine:
|
Machine:
|
||||||
Key:
|
Key:
|
||||||
NotFound: Maschinen Key nicht gefunden
|
NotFound: Maschinen Key nicht gefunden
|
||||||
|
PAT:
|
||||||
|
NotFound: Persönliches Access Token nicht gefunden
|
||||||
NotHuman: Der Benutzer muss eine Person sein
|
NotHuman: Der Benutzer muss eine Person sein
|
||||||
NotMachine: Der Benutzer muss technisch sein
|
NotMachine: Der Benutzer muss technisch sein
|
||||||
|
WrongType: Für diesen Benutzertyp nicht erlaubt
|
||||||
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden
|
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden
|
||||||
Username:
|
Username:
|
||||||
AlreadyExists: Benutzername ist bereits vergeben
|
AlreadyExists: Benutzername ist bereits vergeben
|
||||||
|
@ -66,8 +66,11 @@ Errors:
|
|||||||
Machine:
|
Machine:
|
||||||
Key:
|
Key:
|
||||||
NotFound: Machine key not found
|
NotFound: Machine key not found
|
||||||
|
PAT:
|
||||||
|
NotFound: Personal Access Token not found
|
||||||
NotHuman: The User must be personal
|
NotHuman: The User must be personal
|
||||||
NotMachine: The User must be technical
|
NotMachine: The User must be technical
|
||||||
|
WrongType: Not allowed for this user type
|
||||||
NotAllowedToLink: User is not allowed to link with external login provider
|
NotAllowedToLink: User is not allowed to link with external login provider
|
||||||
Username:
|
Username:
|
||||||
AlreadyExists: Username already taken
|
AlreadyExists: Username already taken
|
||||||
|
@ -66,8 +66,11 @@ Errors:
|
|||||||
Machine:
|
Machine:
|
||||||
Key:
|
Key:
|
||||||
NotFound: Machine Key non trovato
|
NotFound: Machine Key non trovato
|
||||||
|
PAT:
|
||||||
|
NotFound: Personal Access Token non trovato
|
||||||
NotHuman: L'utente deve essere personale
|
NotHuman: L'utente deve essere personale
|
||||||
NotMachine: L'utente deve essere tecnico
|
NotMachine: L'utente deve essere tecnico
|
||||||
|
WrongType: Non consentito per questo tipo di utente
|
||||||
NotAllowedToLink: L'utente non è autorizzato a collegarsi con un provider di accesso esterno
|
NotAllowedToLink: L'utente non è autorizzato a collegarsi con un provider di accesso esterno
|
||||||
Username:
|
Username:
|
||||||
AlreadyExists: Nome utente già preso
|
AlreadyExists: Nome utente già preso
|
||||||
|
@ -21,6 +21,7 @@ type TokenView struct {
|
|||||||
Sequence uint64
|
Sequence uint64
|
||||||
PreferredLanguage string
|
PreferredLanguage string
|
||||||
RefreshTokenID string
|
RefreshTokenID string
|
||||||
|
IsPAT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenSearchRequest struct {
|
type TokenSearchRequest struct {
|
||||||
|
@ -39,6 +39,7 @@ type TokenView struct {
|
|||||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||||
RefreshTokenID string `json:"refreshTokenID,omitempty" gorm:"refresh_token_id"`
|
RefreshTokenID string `json:"refreshTokenID,omitempty" gorm:"refresh_token_id"`
|
||||||
|
IsPAT bool `json:"-" gorm:"is_pat"`
|
||||||
Deactivated bool `json:"-" gorm:"-"`
|
Deactivated bool `json:"-" gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ func TokenViewToModel(token *TokenView) *usr_model.TokenView {
|
|||||||
Sequence: token.Sequence,
|
Sequence: token.Sequence,
|
||||||
PreferredLanguage: token.PreferredLanguage,
|
PreferredLanguage: token.PreferredLanguage,
|
||||||
RefreshTokenID: token.RefreshTokenID,
|
RefreshTokenID: token.RefreshTokenID,
|
||||||
|
IsPAT: token.IsPAT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,13 +109,15 @@ func (t *TokenView) AppendEvent(event *es_models.Event) error {
|
|||||||
t.ChangeDate = event.CreationDate
|
t.ChangeDate = event.CreationDate
|
||||||
t.Sequence = event.Sequence
|
t.Sequence = event.Sequence
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case usr_es_model.UserTokenAdded:
|
case usr_es_model.UserTokenAdded,
|
||||||
|
es_models.EventType(user_repo.PersonalAccessTokenAddedType):
|
||||||
t.setRootData(event)
|
t.setRootData(event)
|
||||||
err := t.setData(event)
|
err := t.setData(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.CreationDate = event.CreationDate
|
t.CreationDate = event.CreationDate
|
||||||
|
t.IsPAT = event.Type == es_models.EventType(user_repo.PersonalAccessTokenAddedType)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
14
migrations/cockroach/V1.110__personal_access_token.sql
Normal file
14
migrations/cockroach/V1.110__personal_access_token.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE auth.tokens ADD COLUMN is_pat BOOLEAN DEFAULT false NOT NULL;
|
||||||
|
|
||||||
|
CREATE TABLE zitadel.projections.personal_access_tokens (
|
||||||
|
id STRING
|
||||||
|
, creation_date TIMESTAMPTZ NOT NULL
|
||||||
|
, change_date TIMESTAMPTZ NOT NULL
|
||||||
|
, resource_owner STRING NOT NULL
|
||||||
|
, sequence INT8 NOT NULL
|
||||||
|
, user_id STRING NOT NULL
|
||||||
|
, expiration TIMESTAMPTZ NOT NULL
|
||||||
|
, scopes STRING[]
|
||||||
|
|
||||||
|
, PRIMARY KEY (id)
|
||||||
|
);
|
@ -668,7 +668,7 @@ service ManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed a machine key
|
// Removes a machine key
|
||||||
rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) {
|
rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
delete: "/users/{user_id}/keys/{key_id}"
|
delete: "/users/{user_id}/keys/{key_id}"
|
||||||
@ -679,6 +679,53 @@ service ManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a personal access token of a (machine) user
|
||||||
|
rpc GetPersonalAccessTokenByIDs(GetPersonalAccessTokenByIDsRequest) returns (GetPersonalAccessTokenByIDsResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/users/{user_id}/pats/{token_id}"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "user.read"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all personal access tokens of a (machine) user which match the query
|
||||||
|
// Limit should always be set, there is a default limit set by the service
|
||||||
|
rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/users/{user_id}/pats/_search"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "user.read"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a new personal access token for a machine user, details should be stored after return
|
||||||
|
rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/users/{user_id}/pats"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "user.write"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes a personal access token
|
||||||
|
rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/users/{user_id}/pats/{token_id}"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "user.write"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Lists all identity providers (social logins) which a human has configured (e.g Google, Microsoft, AD, etc..)
|
// Lists all identity providers (social logins) which a human has configured (e.g Google, Microsoft, AD, etc..)
|
||||||
// Limit should always be set, there is a default limit set by the service
|
// Limit should always be set, there is a default limit set by the service
|
||||||
rpc ListHumanLinkedIDPs(ListHumanLinkedIDPsRequest) returns (ListHumanLinkedIDPsResponse) {
|
rpc ListHumanLinkedIDPs(ListHumanLinkedIDPsRequest) returns (ListHumanLinkedIDPsResponse) {
|
||||||
@ -3398,6 +3445,51 @@ message RemoveMachineKeyResponse {
|
|||||||
zitadel.v1.ObjectDetails details = 1;
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetPersonalAccessTokenByIDsRequest {
|
||||||
|
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
string token_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPersonalAccessTokenByIDsResponse {
|
||||||
|
zitadel.user.v1.PersonalAccessToken token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPersonalAccessTokensRequest {
|
||||||
|
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
//list limitations and ordering
|
||||||
|
zitadel.v1.ListQuery query = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPersonalAccessTokensResponse {
|
||||||
|
zitadel.v1.ListDetails details = 1;
|
||||||
|
repeated zitadel.user.v1.PersonalAccessToken result = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddPersonalAccessTokenRequest {
|
||||||
|
string user_id = 1 [(validate.rules).string.min_len = 1];
|
||||||
|
google.protobuf.Timestamp expiration_date = 2 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2519-04-01T08:45:00.000000Z\"";
|
||||||
|
description: "The date the token will expire and no logins will be possible";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddPersonalAccessTokenResponse {
|
||||||
|
string token_id = 1;
|
||||||
|
string token = 2;
|
||||||
|
zitadel.v1.ObjectDetails details = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemovePersonalAccessTokenRequest {
|
||||||
|
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
string token_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemovePersonalAccessTokenResponse {
|
||||||
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ListHumanLinkedIDPsRequest {
|
message ListHumanLinkedIDPsRequest {
|
||||||
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
//list limitations and ordering
|
//list limitations and ordering
|
||||||
|
@ -571,6 +571,28 @@ message RefreshToken {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message PersonalAccessToken {
|
||||||
|
string id = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906488334\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
zitadel.v1.ObjectDetails details = 2;
|
||||||
|
google.protobuf.Timestamp expiration_date = 3 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "the date the token will expire";
|
||||||
|
example: "\"3019-04-01T08:45:00.000000Z\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
repeated string scopes = 4 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "scopes granted to the token";
|
||||||
|
example: "[\"openid\"]";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
message UserGrant {
|
message UserGrant {
|
||||||
string id = 1 [
|
string id = 1 [
|
||||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user