168 lines
6.3 KiB
Go
168 lines
6.3 KiB
Go
// Copyright 2024 Dolthub, Inc.
|
|
//
|
|
// 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 auth
|
|
|
|
import "github.com/dolthub/doltgresql/utils"
|
|
|
|
// RoleMembership contains all roles that have been granted to other roles.
|
|
type RoleMembership struct {
|
|
Data map[RoleID]map[RoleID]RoleMembershipValue
|
|
}
|
|
|
|
// RoleMembershipValue contains specific membership information between two roles.
|
|
type RoleMembershipValue struct {
|
|
Member RoleID
|
|
Group RoleID
|
|
WithAdminOption bool
|
|
GrantedBy RoleID
|
|
}
|
|
|
|
// NewRoleMembership returns a new *RoleMembership.
|
|
func NewRoleMembership() *RoleMembership {
|
|
return &RoleMembership{
|
|
Data: make(map[RoleID]map[RoleID]RoleMembershipValue),
|
|
}
|
|
}
|
|
|
|
// AddMemberToGroup adds the member role to the group role.
|
|
func AddMemberToGroup(member RoleID, group RoleID, withAdminOption bool, grantedBy RoleID) {
|
|
// We'll perform a sanity check for circular membership. This should be done before this call is made, but since we
|
|
// make assumptions that circular relationships are forbidden (which could lead to infinite loops otherwise), we
|
|
// enforce it here too.
|
|
if groupID, _, _ := IsRoleAMember(group, member); (groupID.IsValid() || member == group) && !globalDatabase.rolesByID[group].IsSuperUser {
|
|
panic("missing validation to prevent circular role relationships")
|
|
}
|
|
groupMap, ok := globalDatabase.roleMembership.Data[member]
|
|
if !ok {
|
|
groupMap = make(map[RoleID]RoleMembershipValue)
|
|
globalDatabase.roleMembership.Data[member] = groupMap
|
|
}
|
|
groupMap[group] = RoleMembershipValue{
|
|
Member: member,
|
|
Group: group,
|
|
WithAdminOption: withAdminOption,
|
|
GrantedBy: grantedBy,
|
|
}
|
|
}
|
|
|
|
// IsRoleAMember returns whether the given role is a member of the group by returning the group's ID. Also returns
|
|
// whether the member was granted WITH ADMIN OPTION, allowing it to grant membership to the group to other roles. A
|
|
// member does not automatically have ADMIN OPTION on itself, therefore this check must be performed.
|
|
func IsRoleAMember(member RoleID, group RoleID) (groupID RoleID, inheritsPrivileges bool, hasWithAdminOption bool) {
|
|
// If the member and group are the same, then we only check for SUPERUSER status to allow WITH ADMIN OPTION
|
|
if member == group {
|
|
return group, true, globalDatabase.rolesByID[member].IsSuperUser
|
|
}
|
|
// Postgres does not allow for circular role membership, so we can recursively check without worry:
|
|
// https://www.postgresql.org/docs/15/catalog-pg-auth-members.html
|
|
if groupMap, ok := globalDatabase.roleMembership.Data[member]; ok {
|
|
for _, value := range groupMap {
|
|
if value.Group == group {
|
|
return group, globalDatabase.rolesByID[member].InheritPrivileges, value.WithAdminOption
|
|
}
|
|
// This recursively walks through memberships
|
|
if groupID, _, hasWithAdminOption = IsRoleAMember(value.Group, group); groupID.IsValid() {
|
|
return groupID, globalDatabase.rolesByID[member].InheritPrivileges, hasWithAdminOption
|
|
}
|
|
}
|
|
}
|
|
// A SUPERUSER has access to everything, and therefore functions as though it's a member of every group
|
|
if globalDatabase.rolesByID[member].IsSuperUser {
|
|
return group, true, true
|
|
}
|
|
return 0, false, false
|
|
}
|
|
|
|
// GetAllGroupsWithMember returns every group that the role is a direct member of. This can also filter by groups that
|
|
// the member has privilege access on.
|
|
func GetAllGroupsWithMember(member RoleID, inheritsPrivilegesOnly bool) []RoleID {
|
|
memberRole, ok := globalDatabase.rolesByID[member]
|
|
if !ok || !memberRole.InheritPrivileges {
|
|
return nil
|
|
}
|
|
groupMap := globalDatabase.roleMembership.Data[member]
|
|
groups := make([]RoleID, 0, len(groupMap))
|
|
for groupID := range groupMap {
|
|
groups = append(groups, groupID)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
// RemoveMemberFromGroup removes the member from the group. If `adminOptionOnly` is true, then only the WITH ADMIN
|
|
// OPTION portion is revoked. If `adminOptionOnly` is false, then the member is fully is removed.
|
|
func RemoveMemberFromGroup(member RoleID, group RoleID, adminOptionOnly bool) {
|
|
if groupMap, ok := globalDatabase.roleMembership.Data[member]; ok {
|
|
if adminOptionOnly {
|
|
value := groupMap[group]
|
|
value.WithAdminOption = false
|
|
groupMap[group] = value
|
|
} else {
|
|
delete(groupMap, group)
|
|
}
|
|
if len(groupMap) == 0 {
|
|
delete(globalDatabase.roleMembership.Data, member)
|
|
}
|
|
}
|
|
}
|
|
|
|
// serialize writes the RoleMembership to the given writer.
|
|
func (membership *RoleMembership) serialize(writer *utils.Writer) {
|
|
// Version 0
|
|
// Write the total number of members
|
|
writer.Uint64(uint64(len(membership.Data)))
|
|
for _, groupMap := range membership.Data {
|
|
// Write the number of groups
|
|
writer.Uint64(uint64(len(groupMap)))
|
|
for _, mapValue := range groupMap {
|
|
// Write the membership information
|
|
writer.Uint64(uint64(mapValue.Member))
|
|
writer.Uint64(uint64(mapValue.Group))
|
|
writer.Bool(mapValue.WithAdminOption)
|
|
writer.Uint64(uint64(mapValue.GrantedBy))
|
|
}
|
|
}
|
|
}
|
|
|
|
// deserialize reads the RoleMembership from the given reader.
|
|
func (membership *RoleMembership) deserialize(version uint32, reader *utils.Reader) {
|
|
membership.Data = make(map[RoleID]map[RoleID]RoleMembershipValue)
|
|
switch version {
|
|
case 0, 1:
|
|
// Read the total number of members
|
|
memberCount := reader.Uint64()
|
|
for memberIdx := uint64(0); memberIdx < memberCount; memberIdx++ {
|
|
// Read the number of groups
|
|
groupCount := reader.Uint64()
|
|
groupMap := make(map[RoleID]RoleMembershipValue)
|
|
var member RoleID
|
|
for groupIdx := uint64(0); groupIdx < groupCount; groupIdx++ {
|
|
// Read the membership information
|
|
value := RoleMembershipValue{}
|
|
value.Member = RoleID(reader.Uint64())
|
|
value.Group = RoleID(reader.Uint64())
|
|
value.WithAdminOption = reader.Bool()
|
|
value.GrantedBy = RoleID(reader.Uint64())
|
|
// Add the information to the map
|
|
groupMap[value.Group] = value
|
|
member = value.Member
|
|
}
|
|
// Add the group map to the data
|
|
membership.Data[member] = groupMap
|
|
}
|
|
default:
|
|
panic("unexpected version in RoleMembership")
|
|
}
|
|
}
|