Merge remote-tracking branch 'origin/main' into feat/audiobooks
# Conflicts: # go.sum
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Silo-Server/silo-server/internal/models"
|
||||
)
|
||||
|
||||
type Permission string
|
||||
|
||||
const PermissionMetadataCuration Permission = "metadata_curation"
|
||||
|
||||
var assignablePermissions = map[Permission]struct{}{
|
||||
PermissionMetadataCuration: {},
|
||||
}
|
||||
|
||||
func assignablePermissionList() []string {
|
||||
out := make([]string, 0, len(assignablePermissions))
|
||||
for permission := range assignablePermissions {
|
||||
out = append(out, string(permission))
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func isAssignablePermission(permission Permission) bool {
|
||||
_, ok := assignablePermissions[permission]
|
||||
return ok
|
||||
}
|
||||
|
||||
func NormalizePermissions(values []string) ([]string, error) {
|
||||
if len(values) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, raw := range values {
|
||||
key := strings.TrimSpace(raw)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
permission := Permission(key)
|
||||
if !isAssignablePermission(permission) {
|
||||
return nil, fmt.Errorf("unknown permission %q", key)
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, key)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func HasAssignedPermission(user *models.User, permission Permission) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
for _, value := range user.Permissions {
|
||||
if value == string(permission) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasEffectivePermission(user *models.User, permission Permission) bool {
|
||||
if user == nil || !user.Enabled {
|
||||
return false
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
return isAssignablePermission(permission)
|
||||
}
|
||||
return HasAssignedPermission(user, permission)
|
||||
}
|
||||
|
||||
func EffectivePermissions(user *models.User) []string {
|
||||
if user == nil || !user.Enabled {
|
||||
return []string{}
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
return assignablePermissionList()
|
||||
}
|
||||
permissions, err := NormalizePermissions(user.Permissions)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
return permissions
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Silo-Server/silo-server/internal/models"
|
||||
)
|
||||
|
||||
func TestNormalizePermissions_DeduplicatesAndSorts(t *testing.T) {
|
||||
got, err := NormalizePermissions([]string{
|
||||
" metadata_curation ",
|
||||
"metadata_curation",
|
||||
"",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizePermissions returned error: %v", err)
|
||||
}
|
||||
want := []string{"metadata_curation"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("permissions = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePermissions_RejectsUnknownPermission(t *testing.T) {
|
||||
if _, err := NormalizePermissions([]string{"server_owner"}); err == nil {
|
||||
t.Fatal("expected unknown permission error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasEffectivePermission_AdminImpliesMetadataCuration(t *testing.T) {
|
||||
user := &models.User{Role: "admin", Enabled: true}
|
||||
if !HasEffectivePermission(user, PermissionMetadataCuration) {
|
||||
t.Fatal("admin should have metadata curation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasEffectivePermission_UserRequiresAssignedPermission(t *testing.T) {
|
||||
user := &models.User{Role: "user", Enabled: true}
|
||||
if HasEffectivePermission(user, PermissionMetadataCuration) {
|
||||
t.Fatal("plain user should not have metadata curation")
|
||||
}
|
||||
user.Permissions = []string{"metadata_curation"}
|
||||
if !HasEffectivePermission(user, PermissionMetadataCuration) {
|
||||
t.Fatal("assigned user should have metadata curation")
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func NewUserRepository(pool *pgxpool.Pool) *UserRepository {
|
||||
|
||||
// allColumns is the list of columns returned by all SELECT queries.
|
||||
// Kept in one place so scanUser stays in sync.
|
||||
const allColumns = `id, email, username, password_hash, local_password_login_enabled, role, enabled,
|
||||
const allColumns = `id, email, username, password_hash, local_password_login_enabled, role, permissions, enabled,
|
||||
library_ids, max_playback_quality, access_policy_revision,
|
||||
max_streams, max_transcodes, max_profiles, download_allowed,
|
||||
download_transcode_allowed, created_at, updated_at`
|
||||
@@ -64,6 +64,7 @@ func scanUser(row pgx.Row) (*models.User, error) {
|
||||
&u.PasswordHash,
|
||||
&u.LocalPasswordLoginEnabled,
|
||||
&u.Role,
|
||||
&u.Permissions,
|
||||
&u.Enabled,
|
||||
&u.LibraryIDs,
|
||||
&u.MaxPlaybackQuality,
|
||||
@@ -97,6 +98,7 @@ func scanUsers(rows pgx.Rows) ([]*models.User, error) {
|
||||
&u.PasswordHash,
|
||||
&u.LocalPasswordLoginEnabled,
|
||||
&u.Role,
|
||||
&u.Permissions,
|
||||
&u.Enabled,
|
||||
&u.LibraryIDs,
|
||||
&u.MaxPlaybackQuality,
|
||||
@@ -133,13 +135,19 @@ func (r *UserRepository) Create(ctx context.Context, input models.CreateUserInpu
|
||||
localPasswordLoginEnabled = *input.LocalPasswordLoginEnabled
|
||||
}
|
||||
|
||||
cols := []string{"email", "username", "password_hash", "local_password_login_enabled", "role", "library_ids", "max_playback_quality"}
|
||||
permissions, err := NormalizePermissions(input.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cols := []string{"email", "username", "password_hash", "local_password_login_enabled", "role", "permissions", "library_ids", "max_playback_quality"}
|
||||
args := []any{
|
||||
input.Email,
|
||||
input.Username,
|
||||
string(hash),
|
||||
localPasswordLoginEnabled,
|
||||
input.Role,
|
||||
permissions,
|
||||
input.LibraryIDs,
|
||||
input.MaxPlaybackQuality,
|
||||
}
|
||||
@@ -245,6 +253,15 @@ func (r *UserRepository) Update(ctx context.Context, id int, input models.Update
|
||||
args = append(args, *input.Role)
|
||||
argIndex++
|
||||
}
|
||||
if input.Permissions != nil {
|
||||
permissions, err := NormalizePermissions(*input.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setClauses = append(setClauses, fmt.Sprintf("permissions = $%d", argIndex))
|
||||
args = append(args, permissions)
|
||||
argIndex++
|
||||
}
|
||||
if input.Enabled != nil {
|
||||
setClauses = append(setClauses, fmt.Sprintf("enabled = $%d", argIndex))
|
||||
args = append(args, *input.Enabled)
|
||||
@@ -292,6 +309,14 @@ func (r *UserRepository) Update(ctx context.Context, id int, input models.Update
|
||||
return err
|
||||
}
|
||||
|
||||
if input.Role != nil ||
|
||||
input.Enabled != nil ||
|
||||
input.LibraryIDs != nil ||
|
||||
input.MaxPlaybackQuality != nil ||
|
||||
input.Permissions != nil {
|
||||
setClauses = append(setClauses, "access_policy_revision = access_policy_revision + 1")
|
||||
}
|
||||
|
||||
// Always bump updated_at.
|
||||
setClauses = append(setClauses, "updated_at = NOW()")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user