This commit is contained in:
binwiederhier
2025-07-21 17:44:00 +02:00
parent 51af114b2e
commit f59df0f40a
13 changed files with 333 additions and 140 deletions

View File

@@ -48,7 +48,8 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provisioned-users", Aliases: []string{"auth_provisioned_users"}, EnvVars: []string{"NTFY_AUTH_PROVISIONED_USERS"}, Usage: "pre-provisioned declarative users"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provision-users", Aliases: []string{"auth_provision_users"}, EnvVars: []string{"NTFY_AUTH_PROVISION_USERS"}, Usage: "pre-provisioned declarative users"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provision-access", Aliases: []string{"auth_provision_access"}, EnvVars: []string{"NTFY_AUTH_PROVISION_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
@@ -155,8 +156,8 @@ func execServe(c *cli.Context) error {
authFile := c.String("auth-file")
authStartupQueries := c.String("auth-startup-queries")
authDefaultAccess := c.String("auth-default-access")
authProvisionedUsersRaw := c.StringSlice("auth-provisioned-users")
//authProvisionedAccessRaw := c.StringSlice("auth-provisioned-access")
authProvisionUsersRaw := c.StringSlice("auth-provision-users")
authProvisionAccessRaw := c.StringSlice("auth-provision-access")
attachmentCacheDir := c.String("attachment-cache-dir")
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
@@ -352,27 +353,13 @@ func execServe(c *cli.Context) error {
if err != nil {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
authProvisionedUsers := make([]*user.User, 0)
for _, userLine := range authProvisionedUsersRaw {
parts := strings.Split(userLine, ":")
if len(parts) != 3 {
return fmt.Errorf("invalid provisioned user %s, expected format: 'name:hash:role'", userLine)
}
username := strings.TrimSpace(parts[0])
passwordHash := strings.TrimSpace(parts[1])
role := user.Role(strings.TrimSpace(parts[2]))
if !user.AllowedUsername(username) {
return fmt.Errorf("invalid provisioned user %s, username invalid", userLine)
} else if passwordHash == "" {
return fmt.Errorf("invalid provisioned user %s, password hash cannot be empty", userLine)
} else if !user.AllowedRole(role) {
return fmt.Errorf("invalid provisioned user %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
}
authProvisionedUsers = append(authProvisionedUsers, &user.User{
Name: username,
Hash: passwordHash,
Role: role,
})
authProvisionUsers, err := parseProvisionUsers(authProvisionUsersRaw)
if err != nil {
return err
}
authProvisionAccess, err := parseProvisionAccess(authProvisionUsers, authProvisionAccessRaw)
if err != nil {
return err
}
// Special case: Unset default
@@ -429,8 +416,8 @@ func execServe(c *cli.Context) error {
conf.AuthFile = authFile
conf.AuthStartupQueries = authStartupQueries
conf.AuthDefault = authDefault
conf.AuthProvisionedUsers = authProvisionedUsers
conf.AuthProvisionedAccess = nil // FIXME
conf.AuthProvisionedUsers = authProvisionUsers
conf.AuthProvisionedAccess = authProvisionAccess
conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
@@ -544,6 +531,76 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
return
}
func parseProvisionUsers(usersRaw []string) ([]*user.User, error) {
provisionUsers := make([]*user.User, 0)
for _, userLine := range usersRaw {
parts := strings.Split(userLine, ":")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid auth-provision-users: %s, expected format: 'name:hash:role'", userLine)
}
username := strings.TrimSpace(parts[0])
passwordHash := strings.TrimSpace(parts[1])
role := user.Role(strings.TrimSpace(parts[2]))
if !user.AllowedUsername(username) {
return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine)
} else if passwordHash == "" {
return nil, fmt.Errorf("invalid auth-provision-users: %s, password hash cannot be empty", userLine)
} else if !user.AllowedRole(role) {
return nil, fmt.Errorf("invalid auth-provision-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
}
provisionUsers = append(provisionUsers, &user.User{
Name: username,
Hash: passwordHash,
Role: role,
Provisioned: true,
})
}
return provisionUsers, nil
}
func parseProvisionAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[string][]*user.Grant, error) {
access := make(map[string][]*user.Grant)
for _, accessLine := range provisionAccessRaw {
parts := strings.Split(accessLine, ":")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid auth-provision-access: %s, expected format: 'user:topic:permission'", accessLine)
}
username := strings.TrimSpace(parts[0])
if username == userEveryone {
username = user.Everyone
}
provisionUser, exists := util.Find(provisionUsers, func(u *user.User) bool {
return u.Name == username
})
if username != user.Everyone {
if !exists {
return nil, fmt.Errorf("invalid auth-provision-access: %s, user %s is not provisioned", accessLine, username)
} else if !user.AllowedUsername(username) {
return nil, fmt.Errorf("invalid auth-provision-access: %s, username %s invalid", accessLine, username)
} else if provisionUser.Role != user.RoleUser {
return nil, fmt.Errorf("invalid auth-provision-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username)
}
}
topic := strings.TrimSpace(parts[1])
if !user.AllowedTopicPattern(topic) {
return nil, fmt.Errorf("invalid auth-provision-access: %s, topic pattern %s invalid", accessLine, topic)
}
permission, err := user.ParsePermission(strings.TrimSpace(parts[2]))
if err != nil {
return nil, fmt.Errorf("invalid auth-provision-access: %s, permission %s invalid, %s", accessLine, parts[2], err.Error())
}
if _, exists := access[username]; !exists {
access[username] = make([]*user.Grant, 0)
}
access[username] = append(access[username], &user.Grant{
TopicPattern: topic,
Permission: permission,
Provisioned: true,
})
}
return access, nil
}
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
newLevelStr, err := inputSource.String("log-level")
if err != nil {