Add API approval broker for gRPC authorization prompts
This commit is contained in:
+162
-80
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.julianfamily.org/keepassgo/apiapproval"
|
||||
"git.julianfamily.org/keepassgo/apitokens"
|
||||
"git.julianfamily.org/keepassgo/clipboard"
|
||||
"git.julianfamily.org/keepassgo/passwords"
|
||||
@@ -33,6 +34,7 @@ type Server struct {
|
||||
lifecycle lifecycleBackend
|
||||
profiles map[string]passwords.Profile
|
||||
clipboard clipboard.Writer
|
||||
approvals *apiapproval.Broker
|
||||
}
|
||||
|
||||
type lifecycleBackend interface {
|
||||
@@ -44,11 +46,17 @@ type lifecycleBackend interface {
|
||||
Unlock(vault.MasterKey) error
|
||||
}
|
||||
|
||||
type modelReplaceableLifecycle interface {
|
||||
lifecycleBackend
|
||||
Replace(vault.Model)
|
||||
}
|
||||
|
||||
func NewServer(model vault.Model, profiles map[string]passwords.Profile, clipboardWriter clipboard.Writer) *Server {
|
||||
return &Server{
|
||||
model: model,
|
||||
profiles: profiles,
|
||||
clipboard: clipboardWriter,
|
||||
approvals: apiapproval.NewBroker(30 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +66,15 @@ func NewServerWithLifecycle(model vault.Model, profiles map[string]passwords.Pro
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) ApprovalBroker() *apiapproval.Broker {
|
||||
return s.approvals
|
||||
}
|
||||
|
||||
func (s *Server) ResolveApproval(id string, outcome apiapproval.Outcome) (apiapproval.Request, error) {
|
||||
request, _, err := s.approvals.Resolve(id, outcome)
|
||||
return request, err
|
||||
}
|
||||
|
||||
func (s *Server) GetSessionStatus(_ context.Context, _ *keepassgov1.GetSessionStatusRequest) (*keepassgov1.GetSessionStatusResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -193,17 +210,15 @@ func mapLifecycleError(operation string, err error) error {
|
||||
}
|
||||
|
||||
func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRequest) (*keepassgov1.ListEntriesResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, req.GetPath()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model := s.visibleModel()
|
||||
model = visibleModel(model)
|
||||
var entries []vault.Entry
|
||||
if strings.TrimSpace(req.GetQuery()) != "" {
|
||||
results := model.Search(req.GetQuery())
|
||||
@@ -226,10 +241,8 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
|
||||
}
|
||||
|
||||
func (s *Server) ListGroups(ctx context.Context, req *keepassgov1.ListGroupsRequest) (*keepassgov1.ListGroupsResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListGroups, req.GetPath()); err != nil {
|
||||
@@ -237,18 +250,17 @@ func (s *Server) ListGroups(ctx context.Context, req *keepassgov1.ListGroupsRequ
|
||||
}
|
||||
|
||||
return &keepassgov1.ListGroupsResponse{
|
||||
Names: s.visibleModel().ChildGroups(req.GetPath()),
|
||||
Names: visibleModel(model).ChildGroups(req.GetPath()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateGroup(ctx context.Context, req *keepassgov1.CreateGroupRequest) (*keepassgov1.CreateGroupResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetParentPath()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
@@ -259,13 +271,12 @@ func (s *Server) CreateGroup(ctx context.Context, req *keepassgov1.CreateGroupRe
|
||||
}
|
||||
|
||||
func (s *Server) RenameGroup(ctx context.Context, req *keepassgov1.RenameGroupRequest) (*keepassgov1.RenameGroupResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetPath()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
@@ -282,13 +293,12 @@ func (s *Server) RenameGroup(ctx context.Context, req *keepassgov1.RenameGroupRe
|
||||
}
|
||||
|
||||
func (s *Server) DeleteGroup(ctx context.Context, req *keepassgov1.DeleteGroupRequest) (*keepassgov1.DeleteGroupResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetPath()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
@@ -314,16 +324,15 @@ func (s *Server) UpsertEntry(ctx context.Context, req *keepassgov1.UpsertEntryRe
|
||||
}
|
||||
|
||||
entry := entryFromProto(req.GetEntry())
|
||||
if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if s.locked {
|
||||
s.mu.Unlock()
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil {
|
||||
s.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
s.model.UpsertEntry(entry)
|
||||
s.dirty = true
|
||||
s.mu.Unlock()
|
||||
@@ -332,18 +341,19 @@ func (s *Server) UpsertEntry(ctx context.Context, req *keepassgov1.UpsertEntryRe
|
||||
}
|
||||
|
||||
func (s *Server) DeleteEntry(ctx context.Context, req *keepassgov1.DeleteEntryRequest) (*keepassgov1.DeleteEntryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
if entry, err := findEntryByID(s.model, req.GetId()); err == nil {
|
||||
if entry, err := findEntryByID(model, req.GetId()); err == nil {
|
||||
if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := s.model.DeleteEntry(req.GetId()); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
@@ -356,15 +366,13 @@ func (s *Server) DeleteEntry(ctx context.Context, req *keepassgov1.DeleteEntryRe
|
||||
}
|
||||
|
||||
func (s *Server) RestoreEntry(ctx context.Context, req *keepassgov1.RestoreEntryRequest) (*keepassgov1.RestoreEntryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
var restored vault.Entry
|
||||
for _, entry := range s.model.RecycleBin {
|
||||
for _, entry := range model.RecycleBin {
|
||||
if entry.ID == req.GetId() {
|
||||
restored = entry
|
||||
break
|
||||
@@ -376,6 +384,9 @@ func (s *Server) RestoreEntry(ctx context.Context, req *keepassgov1.RestoreEntry
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := s.model.RestoreEntry(req.GetId()); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
@@ -388,14 +399,12 @@ func (s *Server) RestoreEntry(ctx context.Context, req *keepassgov1.RestoreEntry
|
||||
}
|
||||
|
||||
func (s *Server) ListEntryHistory(ctx context.Context, req *keepassgov1.ListEntryHistoryRequest) (*keepassgov1.ListEntryHistoryResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetId())
|
||||
entry, err := findEntryByID(model, req.GetId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -413,13 +422,11 @@ func (s *Server) ListEntryHistory(ctx context.Context, req *keepassgov1.ListEntr
|
||||
}
|
||||
|
||||
func (s *Server) RestoreEntryHistory(ctx context.Context, req *keepassgov1.RestoreEntryHistoryRequest) (*keepassgov1.RestoreEntryHistoryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
entry, err := findEntryByID(s.model, req.GetId())
|
||||
entry, err := findEntryByID(model, req.GetId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -427,6 +434,8 @@ func (s *Server) RestoreEntryHistory(ctx context.Context, req *keepassgov1.Resto
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := s.model.RestoreEntryVersion(req.GetId(), int(req.GetHistoryIndex())); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
@@ -523,14 +532,12 @@ func (s *Server) InstantiateTemplate(_ context.Context, req *keepassgov1.Instant
|
||||
}
|
||||
|
||||
func (s *Server) ListAttachments(ctx context.Context, req *keepassgov1.ListAttachmentsRequest) (*keepassgov1.ListAttachmentsResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetEntryId())
|
||||
entry, err := findEntryByID(model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -548,14 +555,11 @@ func (s *Server) ListAttachments(ctx context.Context, req *keepassgov1.ListAttac
|
||||
}
|
||||
|
||||
func (s *Server) UploadAttachment(ctx context.Context, req *keepassgov1.UploadAttachmentRequest) (*keepassgov1.UploadAttachmentResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, index, err := findMutableEntryByID(&s.model, req.GetEntryId())
|
||||
entry, err := findEntryByID(model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -563,6 +567,13 @@ func (s *Server) UploadAttachment(ctx context.Context, req *keepassgov1.UploadAt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
entry, index, err := findMutableEntryByID(&s.model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
if entry.Attachments == nil {
|
||||
entry.Attachments = map[string][]byte{}
|
||||
}
|
||||
@@ -574,14 +585,12 @@ func (s *Server) UploadAttachment(ctx context.Context, req *keepassgov1.UploadAt
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAttachment(ctx context.Context, req *keepassgov1.DownloadAttachmentRequest) (*keepassgov1.DownloadAttachmentResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetEntryId())
|
||||
entry, err := findEntryByID(model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -600,14 +609,11 @@ func (s *Server) DownloadAttachment(ctx context.Context, req *keepassgov1.Downlo
|
||||
}
|
||||
|
||||
func (s *Server) DeleteAttachment(ctx context.Context, req *keepassgov1.DeleteAttachmentRequest) (*keepassgov1.DeleteAttachmentResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, index, err := findMutableEntryByID(&s.model, req.GetEntryId())
|
||||
entry, err := findEntryByID(model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -615,6 +621,13 @@ func (s *Server) DeleteAttachment(ctx context.Context, req *keepassgov1.DeleteAt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
entry, index, err := findMutableEntryByID(&s.model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
if _, ok := entry.Attachments[req.GetName()]; !ok {
|
||||
return nil, status.Error(codes.NotFound, "attachment not found")
|
||||
}
|
||||
@@ -630,11 +643,7 @@ func (s *Server) DeleteAttachment(ctx context.Context, req *keepassgov1.DeleteAt
|
||||
}
|
||||
|
||||
func (s *Server) CopyEntryField(ctx context.Context, req *keepassgov1.CopyEntryFieldRequest) (*keepassgov1.CopyEntryFieldResponse, error) {
|
||||
s.mu.RLock()
|
||||
model := s.model
|
||||
locked := s.locked
|
||||
s.mu.RUnlock()
|
||||
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
@@ -733,10 +742,10 @@ func findMutableEntryByID(model *vault.Model, id string) (vault.Entry, int, erro
|
||||
return vault.Entry{}, -1, vault.ErrEntryNotFound
|
||||
}
|
||||
|
||||
func (s *Server) visibleModel() vault.Model {
|
||||
out := s.model
|
||||
func visibleModel(model vault.Model) vault.Model {
|
||||
out := model
|
||||
out.Entries = nil
|
||||
for _, entry := range s.model.Entries {
|
||||
for _, entry := range model.Entries {
|
||||
token, ok, err := apitokens.TokenFromEntry(entry)
|
||||
if err == nil && ok && token.ID != "" {
|
||||
continue
|
||||
@@ -744,7 +753,7 @@ func (s *Server) visibleModel() vault.Model {
|
||||
out.Entries = append(out.Entries, entry)
|
||||
}
|
||||
out.Groups = nil
|
||||
for _, path := range s.model.Groups {
|
||||
for _, path := range model.Groups {
|
||||
if len(path) >= 2 && path[0] == "Root" && path[1] == "API Tokens" {
|
||||
continue
|
||||
}
|
||||
@@ -753,6 +762,12 @@ func (s *Server) visibleModel() vault.Model {
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) snapshotModel() (vault.Model, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.model, s.locked
|
||||
}
|
||||
|
||||
var timeNow = func() time.Time { return time.Now().UTC() }
|
||||
|
||||
func (s *Server) authenticateRequest(ctx context.Context) (apitokens.Token, error) {
|
||||
@@ -768,7 +783,9 @@ func (s *Server) authenticateRequest(ctx context.Context) (apitokens.Token, erro
|
||||
if !strings.HasPrefix(values[0], prefix) {
|
||||
return apitokens.Token{}, status.Error(codes.Unauthenticated, "invalid bearer token")
|
||||
}
|
||||
s.mu.RLock()
|
||||
tokens, err := apitokens.Entries(s.model)
|
||||
s.mu.RUnlock()
|
||||
if err != nil {
|
||||
return apitokens.Token{}, status.Errorf(codes.Internal, "load api tokens: %v", err)
|
||||
}
|
||||
@@ -789,10 +806,7 @@ func (s *Server) authorizePathRequest(ctx context.Context, op apitokens.Operatio
|
||||
if err != nil {
|
||||
return apitokens.Token{}, err
|
||||
}
|
||||
if apitokens.Evaluate(token, op, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: path}) != apitokens.DecisionAllow {
|
||||
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access is not allowed for this token")
|
||||
}
|
||||
return token, nil
|
||||
return s.authorizeResourceRequest(ctx, token, op, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: path})
|
||||
}
|
||||
|
||||
func (s *Server) authorizeEntryRequest(ctx context.Context, op apitokens.Operation, entry vault.Entry) (apitokens.Token, error) {
|
||||
@@ -800,10 +814,78 @@ func (s *Server) authorizeEntryRequest(ctx context.Context, op apitokens.Operati
|
||||
if err != nil {
|
||||
return apitokens.Token{}, err
|
||||
}
|
||||
if apitokens.Evaluate(token, op, apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: entry.ID, Path: entry.Path}) != apitokens.DecisionAllow {
|
||||
return s.authorizeResourceRequest(ctx, token, op, apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: entry.ID, Path: entry.Path})
|
||||
}
|
||||
|
||||
func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.Token, op apitokens.Operation, resource apitokens.Resource) (apitokens.Token, error) {
|
||||
switch apitokens.Evaluate(token, op, resource) {
|
||||
case apitokens.DecisionAllow:
|
||||
return token, nil
|
||||
case apitokens.DecisionDeny:
|
||||
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access is not allowed for this token")
|
||||
case apitokens.DecisionPrompt:
|
||||
result, err := s.approvals.Request(ctx, token, op, resource)
|
||||
if result.Rule != nil {
|
||||
if persistErr := s.persistApprovalRule(token.ID, *result.Rule); persistErr != nil {
|
||||
return apitokens.Token{}, status.Errorf(codes.Internal, "persist approval decision: %v", persistErr)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case err == nil:
|
||||
return token, nil
|
||||
case errors.Is(err, apiapproval.ErrRequestDenied):
|
||||
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access denied by user approval")
|
||||
case errors.Is(err, apiapproval.ErrRequestCanceled):
|
||||
return apitokens.Token{}, status.Error(codes.Unauthenticated, "authorization request canceled")
|
||||
case errors.Is(err, apiapproval.ErrRequestTimedOut):
|
||||
return apitokens.Token{}, status.Error(codes.DeadlineExceeded, "authorization request timed out")
|
||||
case errors.Is(err, context.Canceled):
|
||||
return apitokens.Token{}, status.Error(codes.Canceled, "authorization request canceled")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return apitokens.Token{}, status.Error(codes.DeadlineExceeded, "authorization request timed out")
|
||||
default:
|
||||
return apitokens.Token{}, status.Errorf(codes.Internal, "await authorization request: %v", err)
|
||||
}
|
||||
default:
|
||||
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access is not allowed for this token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *Server) persistApprovalRule(tokenID string, rule apitokens.PolicyRule) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, entry := range s.model.Entries {
|
||||
token, ok, err := apitokens.TokenFromEntry(entry)
|
||||
if err != nil || !ok || token.ID != tokenID {
|
||||
continue
|
||||
}
|
||||
if !hasPolicyRule(token.Policies, rule) {
|
||||
token.Policies = append(token.Policies, rule)
|
||||
}
|
||||
s.model.Entries[i] = token.Entry(entry.Path)
|
||||
s.dirty = true
|
||||
if lifecycle, ok := s.lifecycle.(modelReplaceableLifecycle); ok {
|
||||
lifecycle.Replace(s.model)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return status.Error(codes.NotFound, "api token entry not found")
|
||||
}
|
||||
|
||||
func hasPolicyRule(rules []apitokens.PolicyRule, target apitokens.PolicyRule) bool {
|
||||
for _, rule := range rules {
|
||||
if rule.Effect != target.Effect || rule.Operation != target.Operation {
|
||||
continue
|
||||
}
|
||||
if rule.Resource.Kind != target.Resource.Kind || rule.Resource.EntryID != target.Resource.EntryID {
|
||||
continue
|
||||
}
|
||||
if slices.Equal(rule.Resource.Path, target.Resource.Path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func copyOperation(target string) apitokens.Operation {
|
||||
|
||||
Reference in New Issue
Block a user