mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: tweak user api definition
This commit is contained in:
@ -200,7 +200,7 @@ paths:
|
||||
- name: filter
|
||||
description: |-
|
||||
Filter is used to filter memos returned in the list.
|
||||
Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']"
|
||||
Format: "creator == users/{uid} && visibilities == ['PUBLIC', 'PROTECTED']"
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
@ -424,6 +424,9 @@ paths:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: reaction.creator
|
||||
description: |-
|
||||
The name of the creator.
|
||||
Format: users/{uid}
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
@ -605,10 +608,7 @@ paths:
|
||||
type: string
|
||||
title: |-
|
||||
The name of the creator.
|
||||
Format: users/{username}
|
||||
creatorId:
|
||||
type: integer
|
||||
format: int32
|
||||
Format: users/{uid}
|
||||
createTime:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -840,7 +840,7 @@ paths:
|
||||
- name: user
|
||||
description: |-
|
||||
The creator of tags.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
@ -866,7 +866,7 @@ paths:
|
||||
- name: tag.creator
|
||||
description: |-
|
||||
The creator of tags.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
@ -908,7 +908,7 @@ paths:
|
||||
- name: user
|
||||
description: |-
|
||||
The creator of tags.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
@ -948,7 +948,7 @@ paths:
|
||||
- name: user
|
||||
description: |-
|
||||
The creator of tags.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
@ -997,6 +997,26 @@ paths:
|
||||
$ref: '#/definitions/v2User'
|
||||
tags:
|
||||
- UserService
|
||||
/api/v2/users/search:
|
||||
get:
|
||||
summary: SearchUsers searches users by filter.
|
||||
operationId: UserService_SearchUsers
|
||||
responses:
|
||||
"200":
|
||||
description: A successful response.
|
||||
schema:
|
||||
$ref: '#/definitions/v2SearchUsersResponse'
|
||||
default:
|
||||
description: An unexpected error response.
|
||||
schema:
|
||||
$ref: '#/definitions/googlerpcStatus'
|
||||
parameters:
|
||||
- name: filter
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
- UserService
|
||||
/api/v2/webhooks:
|
||||
get:
|
||||
summary: ListWebhooks returns a list of webhooks.
|
||||
@ -1282,7 +1302,7 @@ paths:
|
||||
- name: name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1305,7 +1325,7 @@ paths:
|
||||
- name: name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1329,7 +1349,7 @@ paths:
|
||||
- name: name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1352,7 +1372,7 @@ paths:
|
||||
- name: name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1381,7 +1401,7 @@ paths:
|
||||
- name: name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1410,7 +1430,7 @@ paths:
|
||||
- name: name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1434,7 +1454,7 @@ paths:
|
||||
- name: setting.name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1476,7 +1496,7 @@ paths:
|
||||
- name: user.name
|
||||
description: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
@ -1490,6 +1510,7 @@ paths:
|
||||
id:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The system generated uid of the user.
|
||||
role:
|
||||
$ref: '#/definitions/UserRole'
|
||||
username:
|
||||
@ -1597,6 +1618,9 @@ definitions:
|
||||
format: int32
|
||||
creator:
|
||||
type: string
|
||||
title: |-
|
||||
The name of the creator.
|
||||
Format: users/{uid}
|
||||
contentId:
|
||||
type: string
|
||||
reactionType:
|
||||
@ -1632,7 +1656,7 @@ definitions:
|
||||
type: string
|
||||
title: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
locale:
|
||||
type: string
|
||||
description: The preferred locale of the user.
|
||||
@ -2034,10 +2058,7 @@ definitions:
|
||||
type: string
|
||||
title: |-
|
||||
The name of the creator.
|
||||
Format: users/{username}
|
||||
creatorId:
|
||||
type: integer
|
||||
format: int32
|
||||
Format: users/{uid}
|
||||
createTime:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -2123,6 +2144,14 @@ definitions:
|
||||
memoId:
|
||||
type: integer
|
||||
format: int32
|
||||
v2SearchUsersResponse:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
$ref: '#/definitions/v2User'
|
||||
v2SetMemoRelationsResponse:
|
||||
type: object
|
||||
v2SetMemoResourcesResponse:
|
||||
@ -2158,7 +2187,7 @@ definitions:
|
||||
type: string
|
||||
title: |-
|
||||
The creator of tags.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
v2UpdateInboxResponse:
|
||||
type: object
|
||||
properties:
|
||||
@ -2211,10 +2240,11 @@ definitions:
|
||||
type: string
|
||||
title: |-
|
||||
The name of the user.
|
||||
Format: users/{username}
|
||||
Format: users/{uid}
|
||||
id:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The system generated uid of the user.
|
||||
role:
|
||||
$ref: '#/definitions/UserRole'
|
||||
username:
|
||||
|
@ -341,9 +341,13 @@ func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.C
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create memo relation")
|
||||
}
|
||||
if memo.Visibility != apiv2pb.Visibility_PRIVATE && memo.CreatorId != relatedMemo.CreatorID {
|
||||
creatorID, err := ExtractUserIDFromName(memo.Creator)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid memo creator")
|
||||
}
|
||||
if memo.Visibility != apiv2pb.Visibility_PRIVATE && creatorID != relatedMemo.CreatorID {
|
||||
activity, err := s.Store.CreateActivity(ctx, &store.Activity{
|
||||
CreatorID: memo.CreatorId,
|
||||
CreatorID: creatorID,
|
||||
Type: store.ActivityTypeMemoComment,
|
||||
Level: store.ActivityLevelInfo,
|
||||
Payload: &storepb.ActivityPayload{
|
||||
@ -357,7 +361,7 @@ func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.C
|
||||
return nil, status.Errorf(codes.Internal, "failed to create activity")
|
||||
}
|
||||
if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
|
||||
SenderID: memo.CreatorId,
|
||||
SenderID: creatorID,
|
||||
ReceiverID: relatedMemo.CreatorID,
|
||||
Status: store.UNREAD,
|
||||
Message: &storepb.InboxMessage{
|
||||
@ -409,12 +413,12 @@ func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.Li
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.GetUserMemosStatsRequest) (*apiv2pb.GetUserMemosStatsResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.Name)
|
||||
userID, err := ExtractUserIDFromName(request.Name)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username")
|
||||
return nil, errors.Wrap(err, "invalid user name")
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user")
|
||||
@ -534,8 +538,7 @@ func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
|
||||
Id: int32(memo.ID),
|
||||
Name: memo.ResourceName,
|
||||
RowStatus: convertRowStatusFromStore(memo.RowStatus),
|
||||
Creator: fmt.Sprintf("%s%s", UserNamePrefix, creator.Username),
|
||||
CreatorId: int32(memo.CreatorID),
|
||||
Creator: fmt.Sprintf("%s%d", UserNamePrefix, creator.ID),
|
||||
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
|
||||
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
|
||||
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
|
||||
@ -653,12 +656,12 @@ func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store.
|
||||
}
|
||||
}
|
||||
if filter.Creator != nil {
|
||||
username, err := ExtractUsernameFromName(*filter.Creator)
|
||||
userID, err := ExtractUserIDFromName(*filter.Creator)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "invalid creator name")
|
||||
return errors.Wrap(err, "invalid user name")
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "failed to get user")
|
||||
@ -731,11 +734,11 @@ func parseListMemosFilter(expression string) (*ListMemosFilter, error) {
|
||||
return nil, err
|
||||
}
|
||||
callExpr := expr.GetExpr().GetCallExpr()
|
||||
findField(callExpr, filter)
|
||||
findListMemosField(callExpr, filter)
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
|
||||
func findListMemosField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
|
||||
if len(callExpr.Args) == 2 {
|
||||
idExpr := callExpr.Args[0].GetIdentExpr()
|
||||
if idExpr != nil {
|
||||
@ -775,7 +778,7 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
|
||||
for _, arg := range callExpr.Args {
|
||||
callExpr := arg.GetCallExpr()
|
||||
if callExpr != nil {
|
||||
findField(callExpr, filter)
|
||||
findListMemosField(callExpr, filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -796,31 +799,41 @@ func (s *APIV2Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *api
|
||||
}
|
||||
|
||||
func (s *APIV2Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *apiv2pb.Memo, activityType string) error {
|
||||
creatorID, err := ExtractUserIDFromName(memo.Creator)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "invalid memo creator")
|
||||
}
|
||||
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
|
||||
CreatorID: &memo.CreatorId,
|
||||
CreatorID: &creatorID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, hook := range webhooks {
|
||||
payload := convertMemoToWebhookPayload(memo)
|
||||
payload, err := convertMemoToWebhookPayload(memo)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert memo to webhook payload")
|
||||
}
|
||||
payload.ActivityType = activityType
|
||||
payload.URL = hook.Url
|
||||
err := webhook.Post(*payload)
|
||||
if err != nil {
|
||||
if err := webhook.Post(*payload); err != nil {
|
||||
return errors.Wrap(err, "failed to post webhook")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertMemoToWebhookPayload(memo *apiv2pb.Memo) *webhook.WebhookPayload {
|
||||
func convertMemoToWebhookPayload(memo *apiv2pb.Memo) (*webhook.WebhookPayload, error) {
|
||||
creatorID, err := ExtractUserIDFromName(memo.Creator)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid memo creator")
|
||||
}
|
||||
return &webhook.WebhookPayload{
|
||||
CreatorID: memo.CreatorId,
|
||||
CreatorID: creatorID,
|
||||
CreatedTs: time.Now().Unix(),
|
||||
Memo: &webhook.Memo{
|
||||
ID: memo.Id,
|
||||
CreatorID: memo.CreatorId,
|
||||
CreatorID: creatorID,
|
||||
CreatedTs: memo.CreateTime.Seconds,
|
||||
UpdatedTs: memo.UpdateTime.Seconds,
|
||||
Content: memo.Content,
|
||||
@ -851,5 +864,5 @@ func convertMemoToWebhookPayload(memo *apiv2pb.Memo) *webhook.WebhookPayload {
|
||||
return relations
|
||||
}(),
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
@ -43,13 +43,17 @@ func ExtractWorkspaceSettingKeyFromName(name string) (string, error) {
|
||||
return tokens[0], nil
|
||||
}
|
||||
|
||||
// ExtractUsernameFromName returns the username from a resource name.
|
||||
func ExtractUsernameFromName(name string) (string, error) {
|
||||
// ExtractUserIDFromName returns the uid from a resource name.
|
||||
func ExtractUserIDFromName(name string) (int32, error) {
|
||||
tokens, err := GetNameParentTokens(name, UserNamePrefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return 0, err
|
||||
}
|
||||
return tokens[0], nil
|
||||
id, err := util.ConvertStringToInt32(tokens[0])
|
||||
if err != nil {
|
||||
return 0, errors.Errorf("invalid user ID %q", tokens[0])
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ExtractInboxIDFromName returns the inbox ID from a resource name.
|
||||
|
@ -51,12 +51,12 @@ func (s *APIV2Service) BatchUpsertTag(ctx context.Context, request *apiv2pb.Batc
|
||||
}
|
||||
|
||||
func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.User)
|
||||
userID, err := ExtractUserIDFromName(request.User)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
@ -83,12 +83,12 @@ func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRe
|
||||
}
|
||||
|
||||
func (s *APIV2Service) RenameTag(ctx context.Context, request *apiv2pb.RenameTagRequest) (*apiv2pb.RenameTagResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.User)
|
||||
userID, err := ExtractUserIDFromName(request.User)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
@ -148,12 +148,12 @@ func (s *APIV2Service) RenameTag(ctx context.Context, request *apiv2pb.RenameTag
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.Tag.Creator)
|
||||
userID, err := ExtractUserIDFromName(request.Tag.Creator)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
@ -172,12 +172,12 @@ func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTag
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetTagSuggestions(ctx context.Context, request *apiv2pb.GetTagSuggestionsRequest) (*apiv2pb.GetTagSuggestionsResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.User)
|
||||
userID, err := ExtractUserIDFromName(request.User)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
|
@ -8,10 +8,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/exp/slices"
|
||||
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@ -46,13 +48,40 @@ func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersReques
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.Name)
|
||||
func (s *APIV2Service) SearchUsers(ctx context.Context, request *apiv2pb.SearchUsersRequest) (*apiv2pb.SearchUsersResponse, error) {
|
||||
if request.Filter == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "filter is empty")
|
||||
}
|
||||
filter, err := parseSearchUsersFilter(request.Filter)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name is required")
|
||||
return nil, status.Errorf(codes.InvalidArgument, "failed to parse filter: %v", err)
|
||||
}
|
||||
userFind := &store.FindUser{}
|
||||
if filter.Username != nil {
|
||||
userFind.Username = filter.Username
|
||||
}
|
||||
|
||||
users, err := s.Store.ListUsers(ctx, userFind)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to search users: %v", err)
|
||||
}
|
||||
|
||||
response := &apiv2pb.SearchUsersResponse{
|
||||
Users: []*apiv2pb.User{},
|
||||
}
|
||||
for _, user := range users {
|
||||
response.Users = append(response.Users, convertUserFromStore(user))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) {
|
||||
userID, err := ExtractUserIDFromName(request.Name)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &username,
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
@ -76,13 +105,8 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs
|
||||
if currentUser.Role != store.RoleHost {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
username, err := ExtractUsernameFromName(request.User.Name)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name is required")
|
||||
}
|
||||
if !util.ResourceNameMatcher.MatchString(strings.ToLower(username)) {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", username)
|
||||
if !util.ResourceNameMatcher.MatchString(strings.ToLower(request.User.Username)) {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
|
||||
}
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@ -90,7 +114,7 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs
|
||||
}
|
||||
|
||||
user, err := s.Store.CreateUser(ctx, &store.User{
|
||||
Username: username,
|
||||
Username: request.User.Username,
|
||||
Role: convertUserRoleToStore(request.User.Role),
|
||||
Email: request.User.Email,
|
||||
Nickname: request.User.Nickname,
|
||||
@ -107,22 +131,22 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUserRequest) (*apiv2pb.UpdateUserResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.User.Name)
|
||||
userID, err := ExtractUserIDFromName(request.User.Name)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name is required")
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
currentUser, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
}
|
||||
if currentUser.Username != username && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
|
||||
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
}
|
||||
@ -179,19 +203,19 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUserRequest) (*apiv2pb.DeleteUserResponse, error) {
|
||||
username, err := ExtractUsernameFromName(request.Name)
|
||||
userID, err := ExtractUserIDFromName(request.Name)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name is required")
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
currentUser, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
}
|
||||
if currentUser.Username != username && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
|
||||
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
}
|
||||
@ -311,35 +335,15 @@ func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.U
|
||||
}
|
||||
|
||||
func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2pb.ListUserAccessTokensRequest) (*apiv2pb.ListUserAccessTokensResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
currentUser, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
||||
}
|
||||
if user == nil {
|
||||
if currentUser == nil {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
userID := user.ID
|
||||
username, err := ExtractUsernameFromName(request.Name)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name is required")
|
||||
}
|
||||
// List access token for other users need to be verified.
|
||||
if user.Username != username {
|
||||
// Normal users can only list their access tokens.
|
||||
if user.Role == store.RoleUser {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
// The request user must be exist.
|
||||
requestUser, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
|
||||
if requestUser == nil || err != nil {
|
||||
return nil, status.Errorf(codes.NotFound, "fail to find user %s", username)
|
||||
}
|
||||
userID = requestUser.ID
|
||||
}
|
||||
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, currentUser.ID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
|
||||
}
|
||||
@ -493,7 +497,7 @@ func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store
|
||||
|
||||
func convertUserFromStore(user *store.User) *apiv2pb.User {
|
||||
return &apiv2pb.User{
|
||||
Name: fmt.Sprintf("%s%s", UserNamePrefix, user.Username),
|
||||
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
|
||||
Id: user.ID,
|
||||
RowStatus: convertRowStatusFromStore(user.RowStatus),
|
||||
CreateTime: timestamppb.New(time.Unix(user.CreatedTs, 0)),
|
||||
@ -532,3 +536,50 @@ func convertUserRoleToStore(role apiv2pb.User_Role) store.Role {
|
||||
return store.RoleUser
|
||||
}
|
||||
}
|
||||
|
||||
// SearchUsersFilterCELAttributes are the CEL attributes for SearchUsersFilter.
|
||||
var SearchUsersFilterCELAttributes = []cel.EnvOption{
|
||||
cel.Variable("username", cel.StringType),
|
||||
}
|
||||
|
||||
type SearchUsersFilter struct {
|
||||
Username *string
|
||||
}
|
||||
|
||||
func parseSearchUsersFilter(expression string) (*SearchUsersFilter, error) {
|
||||
e, err := cel.NewEnv(SearchUsersFilterCELAttributes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ast, issues := e.Compile(expression)
|
||||
if issues != nil {
|
||||
return nil, errors.Errorf("found issue %v", issues)
|
||||
}
|
||||
filter := &SearchUsersFilter{}
|
||||
expr, err := cel.AstToParsedExpr(ast)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
callExpr := expr.GetExpr().GetCallExpr()
|
||||
findSearchUsersField(callExpr, filter)
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func findSearchUsersField(callExpr *expr.Expr_Call, filter *SearchUsersFilter) {
|
||||
if len(callExpr.Args) == 2 {
|
||||
idExpr := callExpr.Args[0].GetIdentExpr()
|
||||
if idExpr != nil {
|
||||
if idExpr.Name == "username" {
|
||||
username := callExpr.Args[1].GetConstExpr().GetStringValue()
|
||||
filter.Username = &username
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, arg := range callExpr.Args {
|
||||
callExpr := arg.GetCallExpr()
|
||||
if callExpr != nil {
|
||||
findSearchUsersField(callExpr, filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user