mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
CLI: Improve usage descriptions of client/user management commands #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RoleStrings represents user role names mapped to roles.
|
||||
type RoleStrings = map[string]Role
|
||||
type RoleStrings map[string]Role
|
||||
|
||||
// UserRoles maps valid user account roles.
|
||||
var UserRoles = RoleStrings{
|
||||
@@ -20,6 +25,34 @@ var ClientRoles = RoleStrings{
|
||||
string(RoleNone): RoleNone,
|
||||
}
|
||||
|
||||
// Strings returns the roles as string slice.
|
||||
func (m RoleStrings) Strings() []string {
|
||||
result := make([]string, 0, len(m))
|
||||
for r := range m {
|
||||
if r != "" {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns the comma separated roles as string.
|
||||
func (m RoleStrings) String() string {
|
||||
return strings.Join(m.Strings(), ", ")
|
||||
}
|
||||
|
||||
// CliUsageString returns the roles as string for use in CLI usage descriptions.
|
||||
func (m RoleStrings) CliUsageString() string {
|
||||
s := m.Strings()
|
||||
|
||||
if l := len(s); l > 1 {
|
||||
s[l-1] = "or " + s[l-1]
|
||||
}
|
||||
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
|
||||
// Roles grants permissions to roles.
|
||||
type Roles map[Role]Grant
|
||||
|
||||
|
89
internal/auth/acl/roles_test.go
Normal file
89
internal/auth/acl/roles_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRoleStrings_Strings_SortedAndNoEmpty(t *testing.T) {
|
||||
m := RoleStrings{
|
||||
"visitor": RoleVisitor,
|
||||
"": RoleNone,
|
||||
"guest": RoleGuest,
|
||||
"admin": RoleAdmin,
|
||||
}
|
||||
|
||||
got := m.Strings()
|
||||
|
||||
// Expect deterministic, sorted output and no empty entries.
|
||||
assert.Equal(t, []string{"admin", "guest", "visitor"}, got)
|
||||
assert.True(t, sort.StringsAreSorted(got))
|
||||
}
|
||||
|
||||
func TestRoleStrings_String_Join(t *testing.T) {
|
||||
m := RoleStrings{
|
||||
"b": RoleUser,
|
||||
"a": RoleAdmin,
|
||||
}
|
||||
|
||||
// Sorted keys joined by ", ".
|
||||
assert.Equal(t, "a, b", m.String())
|
||||
}
|
||||
|
||||
func TestRoleStrings_CliUsageString(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
assert.Equal(t, "", (RoleStrings{}).CliUsageString())
|
||||
})
|
||||
|
||||
t.Run("single", func(t *testing.T) {
|
||||
m := RoleStrings{"admin": RoleAdmin}
|
||||
assert.Equal(t, "admin", m.CliUsageString())
|
||||
})
|
||||
|
||||
t.Run("two", func(t *testing.T) {
|
||||
m := RoleStrings{"guest": RoleGuest, "admin": RoleAdmin}
|
||||
// Note the comma before "or" matches current implementation.
|
||||
assert.Equal(t, "admin, or guest", m.CliUsageString())
|
||||
})
|
||||
|
||||
t.Run("three", func(t *testing.T) {
|
||||
m := RoleStrings{"visitor": RoleVisitor, "guest": RoleGuest, "admin": RoleAdmin}
|
||||
assert.Equal(t, "admin, guest, or visitor", m.CliUsageString())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoles_Allow(t *testing.T) {
|
||||
t.Run("specific role grant", func(t *testing.T) {
|
||||
roles := Roles{
|
||||
RoleVisitor: GrantViewShared, // denies delete
|
||||
}
|
||||
assert.True(t, roles.Allow(RoleVisitor, ActionView))
|
||||
assert.True(t, roles.Allow(RoleVisitor, ActionDownload))
|
||||
assert.False(t, roles.Allow(RoleVisitor, ActionDelete))
|
||||
})
|
||||
|
||||
t.Run("default fallback used", func(t *testing.T) {
|
||||
roles := Roles{
|
||||
RoleDefault: GrantViewAll, // allows view, denies delete
|
||||
}
|
||||
assert.True(t, roles.Allow(RoleUser, ActionView))
|
||||
assert.False(t, roles.Allow(RoleUser, ActionDelete))
|
||||
})
|
||||
|
||||
t.Run("specific overrides default (no fallback)", func(t *testing.T) {
|
||||
roles := Roles{
|
||||
RoleVisitor: GrantViewShared, // denies delete
|
||||
RoleDefault: GrantFullAccess, // would allow delete, must NOT be used
|
||||
}
|
||||
assert.False(t, roles.Allow(RoleVisitor, ActionDelete))
|
||||
})
|
||||
|
||||
t.Run("no match and no default", func(t *testing.T) {
|
||||
roles := Roles{
|
||||
RoleVisitor: GrantViewShared,
|
||||
}
|
||||
assert.False(t, roles.Allow(RoleUser, ActionView))
|
||||
})
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||
@@ -13,8 +15,7 @@ const (
|
||||
ClientIdUsage = "static client `UID` for test purposes"
|
||||
ClientSecretUsage = "static client `SECRET` for test purposes"
|
||||
ClientNameUsage = "`CLIENT` name to help identify the application"
|
||||
ClientRoleUsage = "client authorization `ROLE`"
|
||||
ClientAuthScope = "client authorization `SCOPES` e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all)"
|
||||
ClientAuthScope = "client authorization `SCOPES`, e.g. metrics or \"vision photos albums\" (\"*\" to allow all)"
|
||||
ClientAuthProvider = "client authentication `PROVIDER`"
|
||||
ClientAuthMethod = "client authentication `METHOD`"
|
||||
ClientAuthExpires = "access token `LIFETIME` in seconds, after which a new token must be requested"
|
||||
@@ -25,6 +26,10 @@ const (
|
||||
ClientSecretInfo = "\nPLEASE WRITE DOWN THE %s CLIENT SECRET, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n"
|
||||
)
|
||||
|
||||
var (
|
||||
ClientRoleUsage = fmt.Sprintf("client authorization `ROLE`, e.g. %s", acl.ClientRoles.CliUsageString())
|
||||
)
|
||||
|
||||
// ClientsCommands configures the client application subcommands.
|
||||
var ClientsCommands = &cli.Command{
|
||||
Name: "clients",
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||
@@ -12,15 +14,18 @@ const (
|
||||
UserNameUsage = "full `NAME` for display in the interface"
|
||||
UserEmailUsage = "unique `EMAIL` address of the user"
|
||||
UserPasswordUsage = "`PASSWORD` for local authentication (8-72 characters)"
|
||||
UserRoleUsage = "user account `ROLE` (admin or guest)"
|
||||
UserAuthUsage = "authentication `PROVIDER` (default, local, oidc or none)"
|
||||
UserAuthIDUsage = "authentication `ID` e.g. Subject ID or Distinguished Name (DN)"
|
||||
UserAuthIDUsage = "authentication `ID`, e.g. Subject ID or Distinguished Name (DN)"
|
||||
UserAdminUsage = "makes user super admin with full access"
|
||||
UserNoLoginUsage = "disables login on the web interface"
|
||||
UserWebDAVUsage = "allows to sync files via WebDAV"
|
||||
UserDisable2FA = "deactivates two-factor authentication"
|
||||
)
|
||||
|
||||
var (
|
||||
UserRoleUsage = fmt.Sprintf("user account `ROLE`, e.g. %s", acl.UserRoles.CliUsageString())
|
||||
)
|
||||
|
||||
// UsersCommands configures the user management subcommands.
|
||||
var UsersCommands = &cli.Command{
|
||||
Name: "users",
|
||||
|
@@ -689,7 +689,7 @@ var Flags = CliFlags{
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "portal-url",
|
||||
Usage: "base `URL` of the cluster portal e.g. https://portal.example.com",
|
||||
Usage: "base `URL` of the cluster portal, e.g. https://portal.example.com",
|
||||
EnvVars: EnvVars("PORTAL_URL"),
|
||||
Hidden: true,
|
||||
}, Tags: []string{Pro}}, {
|
||||
@@ -837,7 +837,7 @@ var Flags = CliFlags{
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "database-server",
|
||||
Aliases: []string{"db-server"},
|
||||
Usage: "database `HOST` incl. port e.g. \"mariadb:3306\" (or socket path)",
|
||||
Usage: "database `HOST` incl. port, e.g. \"mariadb:3306\" (or socket path)",
|
||||
EnvVars: EnvVars("DATABASE_SERVER"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
|
Reference in New Issue
Block a user