This repository has been archived by the owner on Jul 12, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 83
/
bulk_permission.go
156 lines (139 loc) · 4.89 KB
/
bulk_permission.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Copyright 2021 the Exposure Notifications Verification Server authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"fmt"
"time"
"github.com/google/exposure-notifications-verification-server/pkg/rbac"
"github.com/jinzhu/gorm"
)
// BulkPermissionAction is the permission action to take.
type BulkPermissionAction uint8
const (
_ BulkPermissionAction = iota
BulkPermissionActionAdd
BulkPermissionActionRemove
)
// BulkPermission represents a bulk permission operation. This is not actually a
// table in the database.
type BulkPermission struct {
Errorable
RealmID uint
UserIDs []uint
Permissions rbac.Permission
Action BulkPermissionAction
}
// Apply converges the bulk operation. If a user isn't in the realm, no action
// is taken.
//
// For add operations, if the user already has the permission, no action is
// taken. For remove operations, if the user does not have the permission, no
// action is taken.
//
// Other permissions not in the list are unchanged.
func (b *BulkPermission) Apply(db *Database, actor Auditable) error {
// Bulk update is all-or-nothing, so do everything in a transaction.
return db.db.Transaction(func(tx *gorm.DB) error {
// Fetch all current memberships - this is required for re-building implied
// permissions and auditing.
var memberships []*Membership
if err := tx.
Set("gorm:query_option", "FOR UPDATE").
Model(&Membership{}).
Where("realm_id = ?", b.RealmID).
Where("user_id IN (?)", b.UserIDs).
Find(&memberships).
Error; err != nil {
if IsNotFound(err) {
return nil
}
return err
}
// Process each membership individually.
for _, membership := range memberships {
// Users cannot update their own permissions.
if user, ok := actor.(*User); ok && membership.UserID == user.ID {
continue
}
// Compute new permissions.
newPerms, existingPerms := membership.Permissions, membership.Permissions
switch b.Action {
case BulkPermissionActionAdd:
newPerms = newPerms | b.Permissions
case BulkPermissionActionRemove:
newPerms = newPerms &^ b.Permissions
// Re-compute implied permissions. This handles an edge case where
// someone removes an implied permission but not the implying
// permission. For example, if someone bulk-removed a Read but not
// Write, memberships with Write should still retain read because its
// implied.
//
// There's also a weird security edge case here in that we do not check
// if the actor has this permission. In this case, the membership
// already had the permission, so even if the actor doesn't have said
// permission, it's not privilege escalation.
newPerms = rbac.AddImplied(newPerms)
}
// It's possible that no permissions have changed, in which case we don't
// need to save the record or create an audit entry.
if newPerms == existingPerms {
continue
}
if newPerms == 0 {
if err := tx.
Unscoped().
Model(&Membership{}).
Where("realm_id = ?", membership.RealmID).
Where("user_id = ?", membership.UserID).
Delete(&Membership{
RealmID: membership.RealmID,
UserID: membership.UserID,
}).
Error; err != nil {
return fmt.Errorf("failed to delete membership: %w", err)
}
// Generate audit
audit := BuildAuditEntry(actor, "removed user from realm", membership.User, membership.RealmID)
if err := tx.Save(audit).Error; err != nil {
return fmt.Errorf("failed to save audit: %w", err)
}
} else {
// Save the membership.
if err := tx.
Model(&Membership{}).
Where("realm_id = ?", membership.RealmID).
Where("user_id = ?", membership.UserID).
Update("permissions", newPerms).
Error; err != nil {
return fmt.Errorf("failed to save membership: %w", err)
}
// Audit if permissions were changed.
audit := BuildAuditEntry(actor, "updated user permissions", actor, membership.RealmID)
audit.Diff = stringSliceDiff(rbac.PermissionNames(existingPerms), rbac.PermissionNames(newPerms))
if err := tx.Save(audit).Error; err != nil {
return fmt.Errorf("failed to save audit: %w", err)
}
}
// Cascade updated_at on user
if err := tx.
Model(&User{}).
Where("id = ?", membership.UserID).
UpdateColumn("updated_at", time.Now().UTC()).
Error; err != nil {
return fmt.Errorf("failed to update user updated_at: %w", err)
}
}
return nil
})
}