Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
daca63f94e
commit
305e7bac68
38 changed files with 714 additions and 301 deletions
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/report"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// AuthAddFlags specifies the "photoprism auth add" command flags.
|
||||
|
@ -26,7 +27,7 @@ var AuthAddFlags = []cli.Flag{
|
|||
cli.Int64Flag{
|
||||
Name: "expires, e",
|
||||
Usage: "authentication `LIFETIME` in seconds, after which access expires (-1 to disable the limit)",
|
||||
Value: entity.UnixYear,
|
||||
Value: unix.Year,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -4,22 +4,25 @@ import (
|
|||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// Usage hints for the client management subcommands.
|
||||
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)"
|
||||
ClientAuthProvider = "client authentication `PROVIDER`"
|
||||
ClientAuthMethod = "client authentication `METHOD`"
|
||||
ClientAuthExpires = "authentication `LIFETIME` in seconds, after which a new access token must be requested (-1 to disable the limit)"
|
||||
ClientAuthTokens = "maximum 'NUMBER' of access tokens that the client can request (-1 to disable the limit)"
|
||||
ClientRegenerateSecret = "generate a new client secret and display it"
|
||||
ClientAuthExpires = "access token `LIFETIME` in seconds, after which a new token must be requested"
|
||||
ClientAuthTokens = "maximum `NUMBER` of access tokens that the client can request (-1 to disable the limit)"
|
||||
ClientRegenerateSecret = "set a new randomly generated client secret"
|
||||
ClientEnable = "enable client authentication if disabled"
|
||||
ClientDisable = "disable client authentication"
|
||||
ClientSecretInfo = "\nPLEASE WRITE DOWN THE %s CLIENT SECRET, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n"
|
||||
)
|
||||
|
||||
// ClientsCommands configures the client application subcommands.
|
||||
|
@ -39,6 +42,11 @@ var ClientsCommands = cli.Command{
|
|||
|
||||
// ClientAddFlags specifies the "photoprism client add" command flags.
|
||||
var ClientAddFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: ClientIdUsage,
|
||||
Hidden: true,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Usage: ClientNameUsage,
|
||||
|
@ -67,13 +75,18 @@ var ClientAddFlags = []cli.Flag{
|
|||
cli.Int64Flag{
|
||||
Name: "expires, e",
|
||||
Usage: ClientAuthExpires,
|
||||
Value: entity.UnixDay,
|
||||
Value: unix.Day,
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "tokens, t",
|
||||
Usage: ClientAuthTokens,
|
||||
Value: 10,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "secret",
|
||||
Usage: ClientSecretUsage,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
// ClientModFlags specifies the "photoprism client mod" command flags.
|
||||
|
@ -106,13 +119,20 @@ var ClientModFlags = []cli.Flag{
|
|||
cli.Int64Flag{
|
||||
Name: "expires, e",
|
||||
Usage: ClientAuthExpires,
|
||||
Value: unix.Day,
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "tokens, t",
|
||||
Usage: ClientAuthTokens,
|
||||
Value: 10,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "secret",
|
||||
Usage: ClientSecretUsage,
|
||||
Hidden: true,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "regenerate-secret, r",
|
||||
Name: "regenerate",
|
||||
Usage: ClientRegenerateSecret,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
|
|
@ -2,7 +2,6 @@ package commands
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/urfave/cli"
|
||||
|
@ -20,7 +19,7 @@ import (
|
|||
var ClientsAddCommand = cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Registers a new client application",
|
||||
Description: "Specifying a username as argument will assign the client application to a registered user account.",
|
||||
Description: "If you specify a username as argument, the new client will belong to this user and inherit its privileges.",
|
||||
ArgsUsage: "[username]",
|
||||
Flags: ClientAddFlags,
|
||||
Action: clientsAddAction,
|
||||
|
@ -31,7 +30,7 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
conf.MigrateDb(false, nil)
|
||||
|
||||
frm := form.NewClientFromCli(ctx)
|
||||
frm := form.AddClientFromCli(ctx)
|
||||
|
||||
interactive := true
|
||||
|
||||
|
@ -55,11 +54,6 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
frm.ClientName = clean.Name(res)
|
||||
}
|
||||
|
||||
// Set a default client name if no specific name has been provided.
|
||||
if frm.ClientName == "" {
|
||||
frm.ClientName = time.Now().UTC().Format(time.DateTime)
|
||||
}
|
||||
|
||||
if interactive && frm.AuthScope == "" {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Authorization Scope",
|
||||
|
@ -82,23 +76,26 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
client, addErr := entity.AddClient(frm)
|
||||
|
||||
if addErr != nil {
|
||||
return fmt.Errorf("failed to add client: %s", addErr)
|
||||
return addErr
|
||||
} else {
|
||||
log.Infof("successfully registered new client %s", clean.LogQuote(client.ClientName))
|
||||
|
||||
// Display client details.
|
||||
cols := []string{"Client ID", "Client Name", "Authentication Method", "User", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
cols := []string{"Client ID", "Client Name", "Authentication Method", "User", "Role", "Scope", "Enabled", "Access Token Lifetime", "Created At"}
|
||||
rows := make([][]string, 1)
|
||||
|
||||
var authExpires string
|
||||
|
||||
if client.AuthExpires > 0 {
|
||||
authExpires = client.Expires().String()
|
||||
} else {
|
||||
authExpires = report.Never
|
||||
}
|
||||
|
||||
if client.AuthTokens > 0 {
|
||||
authExpires = fmt.Sprintf("%s; up to %d tokens", authExpires, client.AuthTokens)
|
||||
if authExpires != "" {
|
||||
authExpires = fmt.Sprintf("%s; up to %d tokens", authExpires, client.AuthTokens)
|
||||
} else {
|
||||
authExpires = fmt.Sprintf("up to %d tokens", client.AuthTokens)
|
||||
}
|
||||
}
|
||||
|
||||
rows[0] = []string{
|
||||
|
@ -118,16 +115,28 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
if secret, err := client.NewSecret(); err != nil {
|
||||
// Failed to create client secret.
|
||||
return fmt.Errorf("failed to create client secret: %s", err)
|
||||
// Se a random secret or the secret specified in the command flags, if any.
|
||||
var secret, message string
|
||||
var err error
|
||||
|
||||
if secret = frm.Secret(); secret == "" {
|
||||
secret, err = client.NewSecret()
|
||||
message = fmt.Sprintf(ClientSecretInfo, "FOLLOWING RANDOMLY GENERATED")
|
||||
} else {
|
||||
// Show client authentication credentials.
|
||||
fmt.Printf("\nPLEASE WRITE DOWN THE FOLLOWING RANDOMLY GENERATED CLIENT SECRET, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n")
|
||||
result := report.Credentials("Client ID", client.ClientUID, "Client Secret", secret)
|
||||
fmt.Printf("\n%s\n", result)
|
||||
err = client.SetSecret(secret)
|
||||
message = fmt.Sprintf(ClientSecretInfo, "SPECIFIED")
|
||||
}
|
||||
|
||||
// Check if the secret has been saved successfully or return an error otherwise.
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set client secret: %s", err)
|
||||
}
|
||||
|
||||
// Show client authentication credentials.
|
||||
fmt.Printf(message)
|
||||
result := report.Credentials("Client ID", client.ClientUID, "Client Secret", secret)
|
||||
fmt.Printf("\n%s\n", result)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ var ClientsListCommand = cli.Command{
|
|||
// clientsListAction lists registered client applications
|
||||
func clientsListAction(ctx *cli.Context) error {
|
||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
cols := []string{"Client ID", "Client Name", "Authentication Method", "User", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
cols := []string{"Client ID", "Client Name", "Authentication Method", "User", "Role", "Scope", "Enabled", "Access Token Lifetime", "Created At"}
|
||||
|
||||
// Fetch clients from database.
|
||||
clients, err := query.Clients(ctx.Int("n"), 0, "", ctx.Args().First())
|
||||
|
@ -45,14 +45,17 @@ func clientsListAction(ctx *cli.Context) error {
|
|||
// Display report.
|
||||
for i, client := range clients {
|
||||
var authExpires string
|
||||
|
||||
if client.AuthExpires > 0 {
|
||||
authExpires = client.Expires().String()
|
||||
} else {
|
||||
authExpires = report.Never
|
||||
}
|
||||
|
||||
if client.AuthTokens > 0 {
|
||||
authExpires = fmt.Sprintf("%s; up to %d tokens", authExpires, client.AuthTokens)
|
||||
if authExpires != "" {
|
||||
authExpires = fmt.Sprintf("%s; up to %d tokens", authExpires, client.AuthTokens)
|
||||
} else {
|
||||
authExpires = fmt.Sprintf("up to %d tokens", client.AuthTokens)
|
||||
}
|
||||
}
|
||||
|
||||
rows[i] = []string{
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestClientsListCommand(t *testing.T) {
|
|||
// Check command output for plausibility.
|
||||
// t.Logf(output)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "Client ID;Client Name;Authentication Method;User;Role;Scope;Enabled;Authentication Expires;Created At")
|
||||
assert.Contains(t, output, "Client ID;Client Name;Authentication Method;User;Role;Scope;Enabled;Access Token Lifetime;Created At")
|
||||
assert.Contains(t, output, "Monitoring")
|
||||
assert.Contains(t, output, "metrics")
|
||||
assert.NotContains(t, output, "bob")
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/report"
|
||||
)
|
||||
|
@ -26,10 +26,10 @@ func clientsModAction(ctx *cli.Context) error {
|
|||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
conf.MigrateDb(false, nil)
|
||||
|
||||
id := clean.UID(ctx.Args().First())
|
||||
frm := form.ModClientFromCli(ctx)
|
||||
|
||||
// Name or UID provided?
|
||||
if id == "" {
|
||||
if frm.ID() == "" {
|
||||
log.Infof("no valid client id specified")
|
||||
return cli.ShowSubcommandHelp(ctx)
|
||||
}
|
||||
|
@ -37,73 +37,58 @@ func clientsModAction(ctx *cli.Context) error {
|
|||
// Find client record.
|
||||
var client *entity.Client
|
||||
|
||||
client = entity.FindClientByUID(id)
|
||||
client = entity.FindClientByUID(frm.ID())
|
||||
|
||||
if client == nil {
|
||||
return fmt.Errorf("client %s not found", clean.LogQuote(id))
|
||||
return fmt.Errorf("client %s not found", clean.LogQuote(frm.ID()))
|
||||
}
|
||||
|
||||
if name := clean.Name(ctx.String("name")); name != "" {
|
||||
client.ClientName = name
|
||||
}
|
||||
// Update client from form values.
|
||||
client.SetFormValues(frm)
|
||||
|
||||
if ctx.IsSet("method") {
|
||||
client.AuthMethod = authn.Method(ctx.String("method")).String()
|
||||
}
|
||||
|
||||
if ctx.IsSet("scope") {
|
||||
client.SetScope(ctx.String("scope"))
|
||||
}
|
||||
|
||||
if expires := ctx.Int64("expires"); expires != 0 {
|
||||
if expires > entity.UnixMonth {
|
||||
client.AuthExpires = entity.UnixMonth
|
||||
} else if expires > 0 {
|
||||
client.AuthExpires = expires
|
||||
} else if expires <= 0 {
|
||||
client.AuthExpires = entity.UnixHour
|
||||
}
|
||||
}
|
||||
|
||||
if tokens := ctx.Int64("tokens"); tokens != 0 {
|
||||
if tokens > 2147483647 {
|
||||
client.AuthTokens = 2147483647
|
||||
} else if tokens > 0 {
|
||||
client.AuthTokens = tokens
|
||||
} else if tokens < 0 {
|
||||
client.AuthTokens = -1
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.IsSet("disable") && ctx.Bool("disable") {
|
||||
client.AuthEnabled = false
|
||||
if ctx.IsSet("enable") || ctx.IsSet("disable") {
|
||||
client.AuthEnabled = frm.AuthEnabled
|
||||
log.Infof("disabled client authentication")
|
||||
} else if ctx.IsSet("enable") && ctx.Bool("enable") {
|
||||
client.AuthEnabled = true
|
||||
log.Warnf("enabled client authentication")
|
||||
}
|
||||
|
||||
// Save changes.
|
||||
if client.AuthEnabled {
|
||||
log.Infof("client authentication is enabled")
|
||||
} else {
|
||||
log.Warnf("client authentication is disabled")
|
||||
}
|
||||
|
||||
// Update client record if valid.
|
||||
if err := client.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid client settings: %s", err)
|
||||
return fmt.Errorf("invalid values: %s", err)
|
||||
} else if err = client.Save(); err != nil {
|
||||
return fmt.Errorf("failed to update client settings: %s", err)
|
||||
return err
|
||||
} else {
|
||||
log.Infof("client %s has been updated", clean.LogQuote(client.ClientName))
|
||||
}
|
||||
|
||||
// Regenerate and display secret, if requested.
|
||||
if ctx.IsSet("regenerate-secret") && ctx.Bool("regenerate-secret") {
|
||||
if secret, err := client.NewSecret(); err != nil {
|
||||
// Failed to create client secret.
|
||||
return fmt.Errorf("failed to create client secret: %s", err)
|
||||
} else {
|
||||
// Show client authentication credentials.
|
||||
fmt.Printf("\nTHE FOLLOWING RANDOMLY GENERATED CLIENT ID AND SECRET ARE REQUIRED FOR AUTHENTICATION:\n")
|
||||
result := report.Credentials("Client ID", client.ClientUID, "Client Secret", secret)
|
||||
fmt.Printf("\n%s", result)
|
||||
fmt.Printf("\nPLEASE WRITE THE CREDENTIALS DOWN AND KEEP THEM IN A SAFE PLACE, AS THE SECRET CANNOT BE DISPLAYED AGAIN.\n\n")
|
||||
// Change client secret if requested.
|
||||
var secret, message string
|
||||
var err error
|
||||
|
||||
if ctx.IsSet("regenerate") && ctx.Bool("regenerate") {
|
||||
if secret, err = client.NewSecret(); err != nil {
|
||||
return fmt.Errorf("failed to regenerate client secret: %s", err)
|
||||
}
|
||||
|
||||
message = fmt.Sprintf(ClientSecretInfo, "FOLLOWING RANDOMLY GENERATED")
|
||||
} else if secret = frm.Secret(); secret == "" {
|
||||
log.Debugf("client secret remains unchanged")
|
||||
} else if err = client.SetSecret(secret); err != nil {
|
||||
return fmt.Errorf("failed to set client secret: %s", err)
|
||||
} else {
|
||||
message = fmt.Sprintf(ClientSecretInfo, "NEW")
|
||||
}
|
||||
|
||||
// Show new client secret.
|
||||
if secret != "" && err == nil {
|
||||
fmt.Printf(message)
|
||||
result := report.Credentials("Client ID", client.ClientUID, "Client Secret", secret)
|
||||
fmt.Printf("\n%s\n", result)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// ClientUID is the unique ID prefix.
|
||||
|
@ -64,7 +65,7 @@ func NewClient() *Client {
|
|||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "",
|
||||
AuthExpires: UnixHour,
|
||||
AuthExpires: unix.Hour,
|
||||
AuthTokens: 5,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
|
@ -113,9 +114,20 @@ func (m *Client) Name() string {
|
|||
return m.ClientName
|
||||
}
|
||||
|
||||
// SetName sets a custom client name.
|
||||
func (m *Client) SetName(s string) *Client {
|
||||
if s = clean.Name(s); s != "" {
|
||||
m.ClientName = s
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// SetRole sets the client role specified as string.
|
||||
func (m *Client) SetRole(role string) *Client {
|
||||
m.ClientRole = acl.ClientRoles[clean.Role(role)].String()
|
||||
if role != "" {
|
||||
m.ClientRole = acl.ClientRoles[clean.Role(role)].String()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
@ -173,7 +185,7 @@ func (m *Client) UserInfo() string {
|
|||
if m == nil {
|
||||
return ""
|
||||
} else if m.UserUID == "" {
|
||||
return ""
|
||||
return "n/a"
|
||||
} else if m.UserName != "" {
|
||||
return m.UserName
|
||||
}
|
||||
|
@ -242,21 +254,36 @@ func (m *Client) Updates(values interface{}) error {
|
|||
return UnscopedDb().Model(m).Updates(values).Error
|
||||
}
|
||||
|
||||
// NewSecret sets a new secret stored as hash.
|
||||
func (m *Client) NewSecret() (s string, err error) {
|
||||
// NewSecret sets a random client secret and returns it if successful.
|
||||
func (m *Client) NewSecret() (secret string, err error) {
|
||||
if !m.HasUID() {
|
||||
return "", fmt.Errorf("invalid client uid")
|
||||
}
|
||||
|
||||
s = rnd.Base62(32)
|
||||
secret = rnd.ClientSecret()
|
||||
|
||||
pw := NewPassword(m.ClientUID, s, false)
|
||||
|
||||
if err = pw.Save(); err != nil {
|
||||
if err = m.SetSecret(secret); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// SetSecret updates the current client secret or returns an error otherwise.
|
||||
func (m *Client) SetSecret(secret string) (err error) {
|
||||
if !m.HasUID() {
|
||||
return fmt.Errorf("invalid client uid")
|
||||
} else if !rnd.IsClientSecret(secret) {
|
||||
return fmt.Errorf("invalid client secret")
|
||||
}
|
||||
|
||||
pw := NewPassword(m.ClientUID, secret, false)
|
||||
|
||||
if err = pw.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasSecret checks if the given client secret is correct.
|
||||
|
@ -296,19 +323,37 @@ func (m *Client) Provider() authn.ProviderType {
|
|||
return authn.Provider(m.AuthProvider)
|
||||
}
|
||||
|
||||
// SetProvider sets a custom client authentication provider.
|
||||
func (m *Client) SetProvider(provider authn.ProviderType) *Client {
|
||||
if !provider.IsDefault() {
|
||||
m.AuthProvider = provider.String()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Method returns the client authentication method.
|
||||
func (m *Client) Method() authn.MethodType {
|
||||
return authn.Method(m.AuthMethod)
|
||||
}
|
||||
|
||||
// SetMethod sets a custom client authentication method.
|
||||
func (m *Client) SetMethod(method authn.MethodType) *Client {
|
||||
if !method.IsDefault() {
|
||||
m.AuthMethod = method.String()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Scope returns the client authorization scope.
|
||||
func (m *Client) Scope() string {
|
||||
return clean.Scope(m.AuthScope)
|
||||
}
|
||||
|
||||
// SetScope sets the client authorization scope.
|
||||
// SetScope sets a custom client authorization scope.
|
||||
func (m *Client) SetScope(s string) *Client {
|
||||
m.AuthScope = clean.Scope(s)
|
||||
if s = clean.Scope(s); s != "" {
|
||||
m.AuthScope = clean.Scope(s)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
|
@ -318,7 +363,7 @@ func (m *Client) UpdateLastActive() *Client {
|
|||
return m
|
||||
}
|
||||
|
||||
m.LastActive = UnixTime()
|
||||
m.LastActive = unix.Time()
|
||||
|
||||
if err := Db().Model(m).UpdateColumn("LastActive", m.LastActive).Error; err != nil {
|
||||
log.Debugf("client: failed to update %s timestamp (%s)", m.ClientUID, err)
|
||||
|
@ -351,6 +396,29 @@ func (m *Client) Expires() time.Duration {
|
|||
return time.Duration(m.AuthExpires) * time.Second
|
||||
}
|
||||
|
||||
// SetExpires sets a custom auth expiration time in seconds.
|
||||
func (m *Client) SetExpires(i int64) *Client {
|
||||
if i != 0 {
|
||||
m.AuthExpires = i
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Tokens returns maximum number of access tokens this client can create.
|
||||
func (m *Client) Tokens() time.Duration {
|
||||
return time.Duration(m.AuthExpires) * time.Second
|
||||
}
|
||||
|
||||
// SetTokens sets a custom access token limit for this client.
|
||||
func (m *Client) SetTokens(i int64) *Client {
|
||||
if i != 0 {
|
||||
m.AuthTokens = i
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Report returns the entity values as rows.
|
||||
func (m *Client) Report(skipEmpty bool) (rows [][]string, cols []string) {
|
||||
cols = []string{"Name", "Value"}
|
||||
|
@ -380,51 +448,50 @@ func (m *Client) Report(skipEmpty bool) (rows [][]string, cols []string) {
|
|||
// SetFormValues sets the values specified in the form.
|
||||
func (m *Client) SetFormValues(frm form.Client) *Client {
|
||||
if frm.UserUID == "" && frm.UserName == "" {
|
||||
// Ignore.
|
||||
// Client does not belong to a specific user or the user remains unchanged.
|
||||
} else if u := FindUser(User{UserUID: frm.UserUID, UserName: frm.UserName}); u != nil {
|
||||
m.SetUser(u)
|
||||
}
|
||||
|
||||
if frm.ClientName != "" {
|
||||
m.ClientName = frm.Name()
|
||||
// Set custom client UID?
|
||||
if id := frm.ID(); m.ClientUID == "" && id != "" {
|
||||
m.ClientUID = id
|
||||
}
|
||||
|
||||
if frm.ClientRole != "" {
|
||||
m.SetRole(frm.ClientRole)
|
||||
}
|
||||
|
||||
if frm.AuthProvider != "" {
|
||||
m.AuthProvider = frm.Provider().String()
|
||||
}
|
||||
|
||||
if frm.AuthMethod != "" {
|
||||
m.AuthMethod = frm.Method().String()
|
||||
}
|
||||
|
||||
if frm.AuthScope != "" {
|
||||
m.SetScope(frm.AuthScope)
|
||||
}
|
||||
|
||||
if frm.AuthExpires > UnixMonth {
|
||||
m.AuthExpires = UnixMonth
|
||||
} else if frm.AuthExpires > 0 {
|
||||
m.AuthExpires = frm.AuthExpires
|
||||
} else if m.AuthExpires <= 0 {
|
||||
m.AuthExpires = UnixHour
|
||||
}
|
||||
|
||||
if frm.AuthTokens > 2147483647 {
|
||||
m.AuthTokens = 2147483647
|
||||
} else if frm.AuthTokens > 0 {
|
||||
m.AuthTokens = frm.AuthTokens
|
||||
} else if m.AuthTokens < 0 {
|
||||
m.AuthTokens = -1
|
||||
}
|
||||
// Set values from form.
|
||||
m.SetName(frm.Name())
|
||||
m.SetProvider(frm.Provider())
|
||||
m.SetMethod(frm.Method())
|
||||
m.SetScope(frm.Scope())
|
||||
m.SetTokens(frm.Tokens())
|
||||
m.SetExpires(frm.Expires())
|
||||
|
||||
// Enable authentication?
|
||||
if frm.AuthEnabled {
|
||||
m.AuthEnabled = true
|
||||
}
|
||||
|
||||
// Replace empty values with defaults.
|
||||
if m.AuthProvider == "" {
|
||||
m.AuthProvider = authn.ProviderClientCredentials.String()
|
||||
}
|
||||
|
||||
if m.AuthMethod == "" {
|
||||
m.AuthMethod = authn.MethodOAuth2.String()
|
||||
}
|
||||
|
||||
if m.AuthScope == "" {
|
||||
m.AuthScope = "*"
|
||||
}
|
||||
|
||||
if m.AuthExpires <= 0 {
|
||||
m.AuthExpires = unix.Hour
|
||||
}
|
||||
|
||||
if m.AuthTokens <= 0 {
|
||||
m.AuthTokens = -1
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
// AddClient creates a new client and returns it if successful.
|
||||
func AddClient(frm form.Client) (client *Client, err error) {
|
||||
if found := FindClientByUID(frm.ID()); found != nil {
|
||||
return found, fmt.Errorf("client id %s already exists", found.ClientUID)
|
||||
}
|
||||
|
||||
client = NewClient().SetFormValues(frm)
|
||||
|
||||
if err = client.Validate(); err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package entity
|
|||
import (
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
type ClientMap map[string]Client
|
||||
|
@ -37,7 +38,7 @@ var ClientFixtures = ClientMap{
|
|||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: UnixDay,
|
||||
AuthExpires: unix.Day,
|
||||
AuthTokens: -1,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
|
@ -73,7 +74,7 @@ var ClientFixtures = ClientMap{
|
|||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "metrics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthExpires: unix.Hour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
|
@ -91,7 +92,7 @@ var ClientFixtures = ClientMap{
|
|||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodUnknown.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: UnixHour,
|
||||
AuthExpires: unix.Hour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
|
@ -109,7 +110,7 @@ var ClientFixtures = ClientMap{
|
|||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "metrics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthExpires: unix.Hour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
|
@ -128,7 +129,7 @@ var ClientFixtures = ClientMap{
|
|||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "statistics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthExpires: unix.Hour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
|
|
|
@ -380,13 +380,13 @@ func TestClient_Expires(t *testing.T) {
|
|||
|
||||
func TestClient_UserInfo(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
assert.Equal(t, "", NewClient().UserInfo())
|
||||
assert.Equal(t, "n/a", NewClient().UserInfo())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
assert.Equal(t, "alice", ClientFixtures.Pointer("alice").UserInfo())
|
||||
})
|
||||
t.Run("Metrics", func(t *testing.T) {
|
||||
assert.Equal(t, "", ClientFixtures.Pointer("metrics").UserInfo())
|
||||
assert.Equal(t, "n/a", ClientFixtures.Pointer("metrics").UserInfo())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/list"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// SessionPrefix for RefID.
|
||||
|
@ -101,7 +102,7 @@ func (m *Session) Expires(t time.Time) *Session {
|
|||
func DeleteExpiredSessions() (deleted int) {
|
||||
found := Sessions{}
|
||||
|
||||
if err := Db().Where("sess_expires > 0 AND sess_expires < ?", UnixTime()).Find(&found).Error; err != nil {
|
||||
if err := Db().Where("sess_expires > 0 AND sess_expires < ?", unix.Time()).Find(&found).Error; err != nil {
|
||||
event.AuditErr([]string{"failed to fetch expired sessions", "%s"}, err)
|
||||
return deleted
|
||||
}
|
||||
|
@ -782,7 +783,7 @@ func (m *Session) ExpiresIn() int64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
return m.SessExpires - UnixTime()
|
||||
return m.SessExpires - unix.Time()
|
||||
}
|
||||
|
||||
// TimeoutAt returns the time at which the session will expire due to inactivity.
|
||||
|
@ -822,7 +823,7 @@ func (m *Session) UpdateLastActive() *Session {
|
|||
return m
|
||||
}
|
||||
|
||||
m.LastActive = UnixTime()
|
||||
m.LastActive = unix.Time()
|
||||
|
||||
if err := Db().Model(m).UpdateColumn("LastActive", m.LastActive).Error; err != nil {
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "failed to update last active time", "%s"}, m.RefID, err)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// Create a new session cache with an expiration time of 15 minutes.
|
||||
|
@ -31,7 +32,7 @@ func FindSession(id string) (*Session, error) {
|
|||
// Find the session in the cache with a fallback to the database.
|
||||
if cacheData, ok := sessionCache.Get(id); ok && cacheData != nil {
|
||||
if cached := cacheData.(*Session); !cached.Expired() {
|
||||
cached.LastActive = UnixTime()
|
||||
cached.LastActive = unix.Time()
|
||||
return cached, nil
|
||||
} else if err := cached.Delete(); err != nil {
|
||||
event.AuditErr([]string{cached.IP(), "session %s", "failed to delete after expiration", "%s"}, cached.RefID, err)
|
||||
|
|
|
@ -4,11 +4,13 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
func TestNewClientAuthentication(t *testing.T) {
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
sess := NewClientAuthentication("Anonymous", UnixDay, "metrics", nil)
|
||||
sess := NewClientAuthentication("Anonymous", unix.Day, "metrics", nil)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -23,7 +25,7 @@ func TestNewClientAuthentication(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess := NewClientAuthentication("alice", UnixDay, "metrics", user)
|
||||
sess := NewClientAuthentication("alice", unix.Day, "metrics", user)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -38,7 +40,7 @@ func TestNewClientAuthentication(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess := NewClientAuthentication("alice", UnixDay, "", user)
|
||||
sess := NewClientAuthentication("alice", unix.Day, "", user)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -65,7 +67,7 @@ func TestNewClientAuthentication(t *testing.T) {
|
|||
|
||||
func TestAddClientAuthentication(t *testing.T) {
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
sess, err := AddClientAuthentication("", UnixDay, "metrics", nil)
|
||||
sess, err := AddClientAuthentication("", unix.Day, "metrics", nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -82,7 +84,7 @@ func TestAddClientAuthentication(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess, err := AddClientAuthentication("My Client App Token", UnixDay, "metrics", user)
|
||||
sess, err := AddClientAuthentication("My Client App Token", unix.Day, "metrics", user)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
type SessionMap map[string]Session
|
||||
|
@ -29,8 +30,8 @@ var SessionFixtures = SessionMap{
|
|||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"),
|
||||
RefID: "sessxkkcabcd",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessTimeout: unix.Day * 3,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
|
@ -40,7 +41,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("bb8658e779403ae524a188712470060f050054324a8b104e"),
|
||||
RefID: "sess34q3hael",
|
||||
SessTimeout: -1,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
SessExpires: unix.Time() + unix.Day,
|
||||
AuthScope: clean.Scope("*"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodDefault.String(),
|
||||
|
@ -55,7 +56,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("DIbS8T-uyGMe1-R3fmTv-vVaR35"),
|
||||
RefID: "sess6ey1ykya",
|
||||
SessTimeout: -1,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
SessExpires: unix.Time() + unix.Day,
|
||||
AuthScope: clean.Scope("*"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodPersonal.String(),
|
||||
|
@ -70,7 +71,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("5d0rGx-EvsDnV-DcKtYY-HT1aWL"),
|
||||
RefID: "sesshjtgx8qt",
|
||||
SessTimeout: -1,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
SessExpires: unix.Time() + unix.Day,
|
||||
AuthScope: clean.Scope("webdav"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodPersonal.String(),
|
||||
|
@ -85,7 +86,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("778f0f7d80579a072836c65b786145d6e0127505194cc51e"),
|
||||
RefID: "sessjr0ge18d",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
SessExpires: unix.Time() + unix.Day,
|
||||
AuthScope: clean.Scope("metrics photos albums videos"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodDefault.String(),
|
||||
|
@ -100,8 +101,8 @@ var SessionFixtures = SessionMap{
|
|||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"),
|
||||
RefID: "sessxkkcabce",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessTimeout: unix.Day * 3,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
user: UserFixtures.Pointer("bob"),
|
||||
UserUID: UserFixtures.Pointer("bob").UserUID,
|
||||
UserName: UserFixtures.Pointer("bob").UserName,
|
||||
|
@ -110,8 +111,8 @@ var SessionFixtures = SessionMap{
|
|||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"),
|
||||
RefID: "sessxkkcabcf",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessTimeout: unix.Day * 3,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
user: UserFixtures.Pointer("unauthorized"),
|
||||
UserUID: UserFixtures.Pointer("unauthorized").UserUID,
|
||||
UserName: UserFixtures.Pointer("unauthorized").UserName,
|
||||
|
@ -120,8 +121,8 @@ var SessionFixtures = SessionMap{
|
|||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"),
|
||||
RefID: "sessxkkcabcg",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessTimeout: unix.Day * 3,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
user: &Visitor,
|
||||
UserUID: Visitor.UserUID,
|
||||
UserName: Visitor.UserName,
|
||||
|
@ -136,7 +137,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("4ebe1048a7384e1e6af2930b5b6f29795ffab691df47a488"),
|
||||
RefID: "sessaae5cxun",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodDefault.String(),
|
||||
|
@ -149,8 +150,8 @@ var SessionFixtures = SessionMap{
|
|||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4"),
|
||||
RefID: "sessxkkcabch",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessTimeout: unix.Day * 3,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
user: UserFixtures.Pointer("friend"),
|
||||
UserUID: UserFixtures.Pointer("friend").UserUID,
|
||||
UserName: UserFixtures.Pointer("friend").UserName,
|
||||
|
@ -160,7 +161,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("9d8b8801ffa23eb52e08ca7766283799ddfd8dd368212345"),
|
||||
RefID: "sessgh612345",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
|
@ -177,7 +178,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("9d8b8801ffa23eb52e08ca7766283799ddfd8dd368208a9b"),
|
||||
RefID: "sessgh6gjuo1",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodDefault.String(),
|
||||
|
@ -193,7 +194,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("3f9684f7d3dd3d5b84edd43289c7fb5ca32ee73bd0233237"),
|
||||
RefID: "sessyugn54so",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
AuthScope: clean.Scope("settings"),
|
||||
AuthProvider: authn.ProviderAccessToken.String(),
|
||||
AuthMethod: authn.MethodDefault.String(),
|
||||
|
@ -209,7 +210,7 @@ var SessionFixtures = SessionMap{
|
|||
ID: rnd.SessionID("9d8b8801ffa23eb52e08ca7766283799ddfd8dd368212123"),
|
||||
RefID: "sessgh6123yt",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Time() + unix.Week,
|
||||
AuthScope: clean.Scope("statistics"),
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
func TestAuthSession(t *testing.T) {
|
||||
|
@ -275,7 +276,7 @@ func TestSessionLogIn(t *testing.T) {
|
|||
rec := httptest.NewRecorder()
|
||||
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetClientIP(clientIp)
|
||||
|
||||
// Create login form.
|
||||
|
@ -295,7 +296,7 @@ func TestSessionLogIn(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("WrongPassword", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetClientIP(clientIp)
|
||||
|
||||
// Create login form.
|
||||
|
@ -315,7 +316,7 @@ func TestSessionLogIn(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("InvalidUser", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetClientIP(clientIp)
|
||||
|
||||
// Create login form.
|
||||
|
@ -335,7 +336,7 @@ func TestSessionLogIn(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("Unknown user with token", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetClientIP(clientIp)
|
||||
|
||||
// Create login form.
|
||||
|
@ -355,7 +356,7 @@ func TestSessionLogIn(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Unknown user with invalid token", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetClientIP(clientIp)
|
||||
|
||||
// Create login form.
|
||||
|
|
|
@ -11,11 +11,12 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
func TestNewSession(t *testing.T) {
|
||||
t.Run("NoSessionData", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
|
||||
assert.True(t, rnd.IsAuthToken(m.AuthToken()))
|
||||
assert.True(t, rnd.IsSessionID(m.ID))
|
||||
|
@ -27,7 +28,7 @@ func TestNewSession(t *testing.T) {
|
|||
assert.Equal(t, 0, len(m.Data().Tokens))
|
||||
})
|
||||
t.Run("EmptySessionData", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetData(NewSessionData())
|
||||
|
||||
assert.True(t, rnd.IsAuthToken(m.AuthToken()))
|
||||
|
@ -42,7 +43,7 @@ func TestNewSession(t *testing.T) {
|
|||
t.Run("WithSessionData", func(t *testing.T) {
|
||||
data := NewSessionData()
|
||||
data.Tokens = []string{"foo", "bar"}
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
m.SetData(data)
|
||||
|
||||
assert.True(t, rnd.IsAuthToken(m.AuthToken()))
|
||||
|
@ -60,7 +61,7 @@ func TestNewSession(t *testing.T) {
|
|||
|
||||
func TestSession_SetData(t *testing.T) {
|
||||
t.Run("Nil", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m := NewSession(unix.Day, unix.Hour*6)
|
||||
|
||||
assert.NotNil(t, m)
|
||||
|
||||
|
@ -74,7 +75,7 @@ func TestSession_SetData(t *testing.T) {
|
|||
|
||||
func TestSession_Expires(t *testing.T) {
|
||||
t.Run("Set expiry date", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
initialExpiryDate := m.SessExpires
|
||||
m.Expires(time.Date(2035, 01, 15, 12, 30, 0, 0, time.UTC))
|
||||
finalExpiryDate := m.SessExpires
|
||||
|
@ -82,7 +83,7 @@ func TestSession_Expires(t *testing.T) {
|
|||
|
||||
})
|
||||
t.Run("Try to set zero date", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
initialExpiryDate := m.SessExpires
|
||||
m.Expires(time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
finalExpiryDate := m.SessExpires
|
||||
|
@ -92,7 +93,7 @@ func TestSession_Expires(t *testing.T) {
|
|||
|
||||
func TestDeleteExpiredSessions(t *testing.T) {
|
||||
assert.Equal(t, 0, DeleteExpiredSessions())
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
m.Expires(time.Date(2000, 01, 15, 12, 30, 0, 0, time.UTC))
|
||||
m.Save()
|
||||
assert.Equal(t, 1, DeleteExpiredSessions())
|
||||
|
@ -161,7 +162,7 @@ func TestFindSessionByRefID(t *testing.T) {
|
|||
|
||||
func TestSession_Regenerate(t *testing.T) {
|
||||
t.Run("NewSession", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
initialID := m.ID
|
||||
m.Regenerate()
|
||||
finalID := m.ID
|
||||
|
@ -226,8 +227,8 @@ func TestSession_Create(t *testing.T) {
|
|||
assert.Empty(t, m)
|
||||
s := &Session{
|
||||
UserName: "charles",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Day * 3,
|
||||
SessTimeout: unix.Time() + unix.Week,
|
||||
RefID: "sessxkkcxxxx",
|
||||
}
|
||||
|
||||
|
@ -252,8 +253,8 @@ func TestSession_Create(t *testing.T) {
|
|||
|
||||
s := &Session{
|
||||
UserName: "charles",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Day * 3,
|
||||
SessTimeout: unix.Time() + unix.Week,
|
||||
RefID: "123",
|
||||
}
|
||||
|
||||
|
@ -274,8 +275,8 @@ func TestSession_Create(t *testing.T) {
|
|||
|
||||
s := &Session{
|
||||
UserName: "charles",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Day * 3,
|
||||
SessTimeout: unix.Time() + unix.Week,
|
||||
RefID: "sessxkkcxxxx",
|
||||
}
|
||||
|
||||
|
@ -292,8 +293,8 @@ func TestSession_Save(t *testing.T) {
|
|||
assert.Empty(t, m)
|
||||
s := &Session{
|
||||
UserName: "chris",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
SessExpires: unix.Day * 3,
|
||||
SessTimeout: unix.Time() + unix.Week,
|
||||
RefID: "sessxkkcxxxy",
|
||||
}
|
||||
|
||||
|
@ -743,14 +744,14 @@ func TestSession_RedeemToken(t *testing.T) {
|
|||
|
||||
func TestSession_TimedOut(t *testing.T) {
|
||||
t.Run("NewSession", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
assert.False(t, m.TimeoutAt().IsZero())
|
||||
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
|
||||
assert.False(t, m.TimedOut())
|
||||
assert.Greater(t, m.ExpiresIn(), int64(0))
|
||||
})
|
||||
t.Run("NoExpiration", func(t *testing.T) {
|
||||
m := NewSession(0, UnixHour)
|
||||
m := NewSession(0, unix.Hour)
|
||||
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
|
||||
assert.True(t, m.TimeoutAt().IsZero())
|
||||
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
|
||||
|
@ -759,7 +760,7 @@ func TestSession_TimedOut(t *testing.T) {
|
|||
assert.Equal(t, m.ExpiresIn(), int64(0))
|
||||
})
|
||||
t.Run("NoTimeout", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, 0)
|
||||
m := NewSession(unix.Day, 0)
|
||||
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
|
||||
assert.False(t, m.TimeoutAt().IsZero())
|
||||
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
|
||||
|
@ -768,19 +769,19 @@ func TestSession_TimedOut(t *testing.T) {
|
|||
assert.Greater(t, m.ExpiresIn(), int64(0))
|
||||
})
|
||||
t.Run("TimedOut", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
utc := UnixTime()
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
utc := unix.Time()
|
||||
|
||||
m.LastActive = utc - (UnixHour + 1)
|
||||
m.LastActive = utc - (unix.Hour + 1)
|
||||
|
||||
assert.False(t, m.TimeoutAt().IsZero())
|
||||
assert.True(t, m.TimedOut())
|
||||
})
|
||||
t.Run("NotTimedOut", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
utc := UnixTime()
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
utc := unix.Time()
|
||||
|
||||
m.LastActive = utc - (UnixHour - 10)
|
||||
m.LastActive = utc - (unix.Hour - 10)
|
||||
|
||||
assert.False(t, m.TimeoutAt().IsZero())
|
||||
assert.False(t, m.TimedOut())
|
||||
|
@ -789,7 +790,7 @@ func TestSession_TimedOut(t *testing.T) {
|
|||
|
||||
func TestSession_Expired(t *testing.T) {
|
||||
t.Run("NewSession", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
|
||||
assert.False(t, m.ExpiresAt().IsZero())
|
||||
assert.False(t, m.Expired())
|
||||
|
@ -813,9 +814,9 @@ func TestSession_Expired(t *testing.T) {
|
|||
assert.False(t, m.TimedOut())
|
||||
})
|
||||
t.Run("Expired", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
|
||||
utc := UnixTime()
|
||||
utc := unix.Time()
|
||||
|
||||
m.SessExpires = utc - 10
|
||||
|
||||
|
@ -826,8 +827,8 @@ func TestSession_Expired(t *testing.T) {
|
|||
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
|
||||
})
|
||||
t.Run("NotExpired", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
utc := UnixTime()
|
||||
m := NewSession(unix.Day, unix.Hour)
|
||||
utc := unix.Time()
|
||||
|
||||
m.SessExpires = utc + 10
|
||||
|
||||
|
|
|
@ -7,34 +7,11 @@ import (
|
|||
// Day specified as time.Duration to improve readability.
|
||||
const Day = time.Hour * 24
|
||||
|
||||
// UnixMinute is one minute in UnixTime.
|
||||
const UnixMinute int64 = 60
|
||||
|
||||
// UnixHour is one hour in UnixTime.
|
||||
const UnixHour = UnixMinute * 60
|
||||
|
||||
// UnixDay is one day in UnixTime.
|
||||
const UnixDay = UnixHour * 24
|
||||
|
||||
// UnixWeek is one week in UnixTime.
|
||||
const UnixWeek = UnixDay * 7
|
||||
|
||||
// UnixMonth is about one month in UnixTime.
|
||||
const UnixMonth = UnixDay * 31
|
||||
|
||||
// UnixYear is about one year in UnixTime.
|
||||
const UnixYear = UnixDay * 365
|
||||
|
||||
// UTC returns the current Coordinated Universal Time (UTC).
|
||||
func UTC() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// UnixTime returns the current time in seconds since January 1, 1970 UTC.
|
||||
func UnixTime() int64 {
|
||||
return UTC().Unix()
|
||||
}
|
||||
|
||||
// TimeStamp returns the current timestamp in UTC rounded to seconds.
|
||||
func TimeStamp() time.Time {
|
||||
return UTC().Truncate(time.Second)
|
||||
|
|
|
@ -3,15 +3,19 @@ package form
|
|||
import (
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// Client represents client application settings.
|
||||
type Client struct {
|
||||
UserUID string `json:"UserUID,omitempty" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
ClientID string `json:"ClientID,omitempty" yaml:"ClientID,omitempty"`
|
||||
ClientSecret string `json:"ClientSecret,omitempty" yaml:"ClientSecret,omitempty"`
|
||||
ClientName string `json:"ClientName,omitempty" yaml:"ClientName,omitempty"`
|
||||
ClientRole string `json:"ClientRole,omitempty" yaml:"ClientRole,omitempty"`
|
||||
AuthProvider string `json:"AuthProvider,omitempty" yaml:"AuthProvider,omitempty"`
|
||||
|
@ -27,7 +31,10 @@ func NewClient() Client {
|
|||
return Client{
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
ClientID: "",
|
||||
ClientSecret: "",
|
||||
ClientName: "",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "",
|
||||
|
@ -37,29 +44,124 @@ func NewClient() Client {
|
|||
}
|
||||
}
|
||||
|
||||
// NewClientFromCli creates a new form with values from a CLI context.
|
||||
func NewClientFromCli(ctx *cli.Context) Client {
|
||||
// AddClientFromCli creates a new form for adding a client with values from the specified CLI context.
|
||||
func AddClientFromCli(ctx *cli.Context) Client {
|
||||
f := NewClient()
|
||||
|
||||
f.ClientName = clean.Name(ctx.String("name"))
|
||||
f.ClientRole = clean.Name(ctx.String("role"))
|
||||
f.AuthProvider = authn.Provider(ctx.String("provider")).String()
|
||||
f.AuthMethod = authn.Method(ctx.String("method")).String()
|
||||
f.AuthScope = clean.Scope(ctx.String("scope"))
|
||||
|
||||
if authn.MethodOAuth2.NotEqual(f.AuthMethod) {
|
||||
f.AuthScope = "webdav"
|
||||
}
|
||||
|
||||
if user := clean.Username(ctx.Args().First()); rnd.IsUID(user, 'u') {
|
||||
f.UserUID = user
|
||||
} else if user != "" {
|
||||
f.UserName = user
|
||||
}
|
||||
|
||||
if ctx.IsSet("id") {
|
||||
f.ClientID = ctx.String("id")
|
||||
}
|
||||
|
||||
if ctx.IsSet("secret") {
|
||||
f.ClientSecret = ctx.String("secret")
|
||||
}
|
||||
|
||||
if ctx.IsSet("name") {
|
||||
f.ClientName = clean.Name(ctx.String("name"))
|
||||
}
|
||||
|
||||
if f.ClientName == "" {
|
||||
f.ClientName = rnd.Name()
|
||||
}
|
||||
|
||||
f.ClientRole = clean.Name(ctx.String("role"))
|
||||
|
||||
if f.ClientRole == "" {
|
||||
f.ClientRole = acl.RoleClient.String()
|
||||
}
|
||||
|
||||
f.AuthProvider = authn.Provider(ctx.String("provider")).String()
|
||||
|
||||
if f.AuthProvider == "" {
|
||||
f.AuthProvider = authn.ProviderClientCredentials.String()
|
||||
}
|
||||
|
||||
f.AuthMethod = authn.Method(ctx.String("method")).String()
|
||||
|
||||
if f.AuthMethod == "" {
|
||||
f.AuthMethod = authn.MethodOAuth2.String()
|
||||
}
|
||||
|
||||
f.AuthScope = clean.Scope(ctx.String("scope"))
|
||||
|
||||
if f.AuthScope == "" {
|
||||
f.AuthScope = "*"
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// ModClientFromCli creates a new form for modifying a client with values from the specified CLI context.
|
||||
func ModClientFromCli(ctx *cli.Context) Client {
|
||||
f := Client{}
|
||||
|
||||
f.ClientID = clean.UID(ctx.Args().First())
|
||||
|
||||
if ctx.IsSet("secret") {
|
||||
f.ClientSecret = ctx.String("secret")
|
||||
}
|
||||
|
||||
if ctx.IsSet("name") {
|
||||
f.ClientName = clean.Name(ctx.String("name"))
|
||||
}
|
||||
|
||||
if ctx.IsSet("role") {
|
||||
f.ClientRole = clean.Name(ctx.String("role"))
|
||||
}
|
||||
|
||||
if ctx.IsSet("provider") {
|
||||
f.AuthProvider = authn.Provider(ctx.String("provider")).String()
|
||||
}
|
||||
|
||||
if ctx.IsSet("method") {
|
||||
f.AuthMethod = authn.Method(ctx.String("method")).String()
|
||||
}
|
||||
|
||||
if ctx.IsSet("scope") {
|
||||
f.AuthScope = clean.Scope(ctx.String("scope"))
|
||||
}
|
||||
|
||||
if ctx.IsSet("expires") {
|
||||
f.AuthExpires = ctx.Int64("expires")
|
||||
}
|
||||
|
||||
if ctx.IsSet("tokens") {
|
||||
f.AuthTokens = ctx.Int64("tokens")
|
||||
}
|
||||
|
||||
if ctx.Bool("enable") {
|
||||
f.AuthEnabled = true
|
||||
} else if ctx.Bool("disable") {
|
||||
f.AuthEnabled = false
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// ID returns the client id, if any.
|
||||
func (f *Client) ID() string {
|
||||
if !rnd.IsUID(f.ClientID, 'c') {
|
||||
return ""
|
||||
}
|
||||
|
||||
return f.ClientID
|
||||
}
|
||||
|
||||
// Secret returns the client secret, if any.
|
||||
func (f *Client) Secret() string {
|
||||
if !rnd.IsClientSecret(f.ClientSecret) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return f.ClientSecret
|
||||
}
|
||||
|
||||
// Name returns the sanitized client name.
|
||||
func (f *Client) Name() string {
|
||||
return clean.Name(f.ClientName)
|
||||
|
@ -84,3 +186,29 @@ func (f *Client) Method() authn.MethodType {
|
|||
func (f Client) Scope() string {
|
||||
return clean.Scope(f.AuthScope)
|
||||
}
|
||||
|
||||
// Expires returns the access token expiry time in seconds or 0 if not specified.
|
||||
func (f Client) Expires() int64 {
|
||||
if f.AuthExpires > unix.Month {
|
||||
return unix.Month
|
||||
} else if f.AuthExpires > 0 {
|
||||
return f.AuthExpires
|
||||
} else if f.AuthExpires < 0 {
|
||||
return unix.Hour
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Tokens returns the access token limit or 0 if not specified.
|
||||
func (f Client) Tokens() int64 {
|
||||
if f.AuthTokens > 2147483647 {
|
||||
return 2147483647
|
||||
} else if f.AuthTokens > 0 {
|
||||
return f.AuthTokens
|
||||
} else if f.AuthTokens < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -20,23 +20,74 @@ func TestNewClient(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNewClientFromCli(t *testing.T) {
|
||||
func TestAddClientFromCli(t *testing.T) {
|
||||
// Specify command flags.
|
||||
flags := flag.NewFlagSet("test", 0)
|
||||
flags.String("name", "(default)", "Usage")
|
||||
flags.String("scope", "(default)", "Usage")
|
||||
flags.String("provider", "(default)", "Usage")
|
||||
flags.String("method", "(default)", "Usage")
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.String("name", "Test", "")
|
||||
globalSet.String("scope", "*", "")
|
||||
globalSet.String("provider", "client_credentials", "")
|
||||
globalSet.String("method", "totp", "")
|
||||
// Create new context with flags.
|
||||
ctx := cli.NewContext(cli.NewApp(), flags, nil)
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Version = "0.0.0"
|
||||
// Set flag values.
|
||||
assert.NoError(t, ctx.Set("name", "Test"))
|
||||
assert.NoError(t, ctx.Set("scope", "*"))
|
||||
assert.NoError(t, ctx.Set("provider", "client_credentials"))
|
||||
assert.NoError(t, ctx.Set("method", "totp"))
|
||||
|
||||
c := cli.NewContext(app, globalSet, nil)
|
||||
t.Logf("ARGS: %#v", ctx.Args())
|
||||
|
||||
client := NewClientFromCli(c)
|
||||
// Check flag values.
|
||||
assert.True(t, ctx.IsSet("name"))
|
||||
assert.Equal(t, "Test", ctx.String("name"))
|
||||
assert.True(t, ctx.IsSet("provider"))
|
||||
assert.Equal(t, "client_credentials", ctx.String("provider"))
|
||||
|
||||
// Set form values.
|
||||
client := AddClientFromCli(ctx)
|
||||
|
||||
// Check form values.
|
||||
assert.Equal(t, authn.ProviderClientCredentials, client.Provider())
|
||||
assert.Equal(t, authn.MethodTOTP, client.Method())
|
||||
assert.Equal(t, "webdav", client.Scope())
|
||||
assert.Equal(t, "*", client.Scope())
|
||||
assert.Equal(t, "Test", client.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModClientFromCli(t *testing.T) {
|
||||
// Specify command flags.
|
||||
flags := flag.NewFlagSet("test", 0)
|
||||
flags.String("name", "(default)", "Usage")
|
||||
flags.String("scope", "(default)", "Usage")
|
||||
flags.String("provider", "(default)", "Usage")
|
||||
flags.String("method", "(default)", "Usage")
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
// Create new context with flags.
|
||||
ctx := cli.NewContext(cli.NewApp(), flags, nil)
|
||||
|
||||
// Set flag values.
|
||||
assert.NoError(t, ctx.Set("name", "Test"))
|
||||
assert.NoError(t, ctx.Set("scope", "*"))
|
||||
assert.NoError(t, ctx.Set("provider", "client_credentials"))
|
||||
assert.NoError(t, ctx.Set("method", "totp"))
|
||||
|
||||
// Check flag values.
|
||||
assert.True(t, ctx.IsSet("name"))
|
||||
assert.Equal(t, "Test", ctx.String("name"))
|
||||
assert.True(t, ctx.IsSet("provider"))
|
||||
assert.Equal(t, "client_credentials", ctx.String("provider"))
|
||||
|
||||
// Set form values.
|
||||
client := ModClientFromCli(ctx)
|
||||
|
||||
// Check form values.
|
||||
assert.Equal(t, authn.ProviderClientCredentials, client.Provider())
|
||||
assert.Equal(t, authn.MethodTOTP, client.Method())
|
||||
assert.Equal(t, "*", client.Scope())
|
||||
assert.Equal(t, "Test", client.Name())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// Session finds an existing session by its id.
|
||||
|
@ -31,7 +32,7 @@ func Sessions(limit, offset int, sortOrder, search string) (result entity.Sessio
|
|||
search = strings.TrimSpace(search)
|
||||
|
||||
if search == "expired" {
|
||||
stmt = stmt.Where("sess_expires > 0 AND sess_expires < ?", entity.UnixTime())
|
||||
stmt = stmt.Where("sess_expires > 0 AND sess_expires < ?", unix.Time())
|
||||
} else if rnd.IsSessionID(search) {
|
||||
stmt = stmt.Where("id = ?", search)
|
||||
} else if rnd.IsAuthToken(search) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
// Save updates the client session or creates a new one if needed.
|
||||
|
@ -15,7 +16,7 @@ func (s *Session) Save(m *entity.Session) (*entity.Session, error) {
|
|||
}
|
||||
|
||||
// Update last active timestamp.
|
||||
m.LastActive = entity.UnixTime()
|
||||
m.LastActive = unix.Time()
|
||||
|
||||
// Save session.
|
||||
err := m.Save()
|
||||
|
|
|
@ -21,6 +21,11 @@ const (
|
|||
MethodUnknown MethodType = ""
|
||||
)
|
||||
|
||||
// IsUnknown checks if the method is unknown.
|
||||
func (t MethodType) IsUnknown() bool {
|
||||
return t == ""
|
||||
}
|
||||
|
||||
// IsDefault checks if this is the default method.
|
||||
func (t MethodType) IsDefault() bool {
|
||||
return t.String() == MethodDefault.String()
|
||||
|
|
|
@ -43,6 +43,11 @@ var ClientProviders = list.List{
|
|||
string(ProviderAccessToken),
|
||||
}
|
||||
|
||||
// IsUnknown checks if the provider is unknown.
|
||||
func (t ProviderType) IsUnknown() bool {
|
||||
return t == ""
|
||||
}
|
||||
|
||||
// IsRemote checks if the provider is external.
|
||||
func (t ProviderType) IsRemote() bool {
|
||||
return list.Contains(RemoteProviders, string(t))
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -48,8 +50,8 @@ var (
|
|||
func CacheControlMaxAge(maxAge int, public bool) string {
|
||||
if maxAge < 0 {
|
||||
return CacheControlNoCache
|
||||
} else if maxAge > 31536000 {
|
||||
maxAge = 31536000
|
||||
} else if maxAge > unix.YearInt {
|
||||
maxAge = unix.YearInt
|
||||
}
|
||||
|
||||
switch {
|
||||
|
|
|
@ -6,8 +6,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/unix"
|
||||
)
|
||||
|
||||
func TestCacheControlMaxAge(t *testing.T) {
|
||||
|
@ -15,27 +16,27 @@ func TestCacheControlMaxAge(t *testing.T) {
|
|||
assert.Equal(t, CacheControlPrivateDefault, CacheControlMaxAge(0, false))
|
||||
assert.Equal(t, "no-cache", CacheControlMaxAge(-1, false))
|
||||
assert.Equal(t, "private, max-age=1", CacheControlMaxAge(1, false))
|
||||
assert.Equal(t, "private, max-age=31536000", CacheControlMaxAge(31536000, false))
|
||||
assert.Equal(t, "private, max-age=31536000", CacheControlMaxAge(unix.YearInt, false))
|
||||
assert.Equal(t, "private, max-age=31536000", CacheControlMaxAge(1231536000, false))
|
||||
})
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
assert.Equal(t, CacheControlPublicDefault, CacheControlMaxAge(0, true))
|
||||
assert.Equal(t, "no-cache", CacheControlMaxAge(-1, true))
|
||||
assert.Equal(t, "public, max-age=1", CacheControlMaxAge(1, true))
|
||||
assert.Equal(t, "public, max-age=31536000", CacheControlMaxAge(31536000, true))
|
||||
assert.Equal(t, "public, max-age=31536000", CacheControlMaxAge(unix.YearInt, true))
|
||||
assert.Equal(t, "public, max-age=31536000", CacheControlMaxAge(1231536000, true))
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkTestCacheControlMaxAge(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = CacheControlMaxAge(31536000, false)
|
||||
_ = CacheControlMaxAge(unix.YearInt, false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTestCacheControlMaxAgeImmutable(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = CacheControlMaxAge(31536000, false) + ", " + CacheControlImmutable
|
||||
_ = CacheControlMaxAge(unix.YearInt, false) + ", " + CacheControlImmutable
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,7 +49,7 @@ func TestSetCacheControl(t *testing.T) {
|
|||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
SetCacheControl(c, 31536000, false)
|
||||
SetCacheControl(c, unix.YearInt, false)
|
||||
assert.Equal(t, "private, max-age=31536000", c.Writer.Header().Get(CacheControl))
|
||||
})
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
|
@ -59,7 +60,7 @@ func TestSetCacheControl(t *testing.T) {
|
|||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
SetCacheControl(c, 31536000, true)
|
||||
SetCacheControl(c, unix.YearInt, true)
|
||||
assert.Equal(t, "public, max-age=31536000", c.Writer.Header().Get(CacheControl))
|
||||
})
|
||||
t.Run("NoCache", func(t *testing.T) {
|
||||
|
@ -84,7 +85,7 @@ func TestSetCacheControlImmutable(t *testing.T) {
|
|||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
SetCacheControlImmutable(c, 31536000, false)
|
||||
SetCacheControlImmutable(c, unix.YearInt, false)
|
||||
assert.Equal(t, "private, max-age=31536000, immutable", c.Writer.Header().Get(CacheControl))
|
||||
})
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
|
@ -95,7 +96,7 @@ func TestSetCacheControlImmutable(t *testing.T) {
|
|||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
SetCacheControlImmutable(c, 31536000, true)
|
||||
SetCacheControlImmutable(c, unix.YearInt, true)
|
||||
assert.Equal(t, "public, max-age=31536000, immutable", c.Writer.Header().Get(CacheControl))
|
||||
})
|
||||
t.Run("PublicDefault", func(t *testing.T) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Package sev provides event importance levels and parsers.
|
||||
Package level provides constants and abstractions for log levels and severities.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
|
||||
|
@ -22,16 +22,4 @@ want to support our work, or just want to say hello.
|
|||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package sev
|
||||
|
||||
// Level represents the severity of an event.
|
||||
type Level uint8
|
||||
|
||||
// String returns the severity level as a string, e.g. Alert becomes "alert".
|
||||
func (level Level) String() string {
|
||||
if b, err := level.MarshalText(); err == nil {
|
||||
return string(b)
|
||||
} else {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
package level
|
|
@ -1,11 +1,12 @@
|
|||
package sev
|
||||
package level
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Severity levels.
|
||||
const (
|
||||
Emergency Level = iota
|
||||
Emergency Severity = iota
|
||||
Alert
|
||||
Critical
|
||||
Error
|
||||
|
@ -15,7 +16,8 @@ const (
|
|||
Debug
|
||||
)
|
||||
|
||||
var Levels = []Level{
|
||||
// Levels contains the valid severity levels.
|
||||
var Levels = []Severity{
|
||||
Emergency,
|
||||
Alert,
|
||||
Critical,
|
||||
|
@ -27,7 +29,7 @@ var Levels = []Level{
|
|||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (level *Level) UnmarshalText(text []byte) error {
|
||||
func (level *Severity) UnmarshalText(text []byte) error {
|
||||
l, err := Parse(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -38,7 +40,8 @@ func (level *Level) UnmarshalText(text []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (level Level) MarshalText() ([]byte, error) {
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (level Severity) MarshalText() ([]byte, error) {
|
||||
switch level {
|
||||
case Debug:
|
||||
return []byte("debug"), nil
|
||||
|
@ -61,7 +64,8 @@ func (level Level) MarshalText() ([]byte, error) {
|
|||
return nil, fmt.Errorf("not a valid severity level %d", level)
|
||||
}
|
||||
|
||||
func (level Level) Status() string {
|
||||
// Status returns the severity level as an info string for reports.
|
||||
func (level Severity) Status() string {
|
||||
switch level {
|
||||
case Warning:
|
||||
return "warning"
|
|
@ -1,10 +1,10 @@
|
|||
package sev
|
||||
package level
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
// LogLevel takes a logrus log level and returns the severity.
|
||||
func LogLevel(lvl logrus.Level) Level {
|
||||
switch lvl {
|
||||
// Logrus takes a logrus.Level and returns the corresponding Severity.
|
||||
func Logrus(level logrus.Level) Severity {
|
||||
switch level {
|
||||
case logrus.PanicLevel:
|
||||
return Alert
|
||||
case logrus.FatalLevel:
|
|
@ -1,13 +1,13 @@
|
|||
package sev
|
||||
package level
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse takes a string level and returns the severity constant.
|
||||
func Parse(lvl string) (Level, error) {
|
||||
switch strings.ToLower(lvl) {
|
||||
// Parse takes a string and returns the corresponding severity, if any.
|
||||
func Parse(level string) (Severity, error) {
|
||||
switch strings.ToLower(level) {
|
||||
case "emergency", "emerg", "panic":
|
||||
return Emergency, nil
|
||||
case "fatal", "alert":
|
||||
|
@ -26,6 +26,6 @@ func Parse(lvl string) (Level, error) {
|
|||
return Debug, nil
|
||||
}
|
||||
|
||||
var l Level
|
||||
return l, fmt.Errorf("not a valid Level: %q", lvl)
|
||||
var l Severity
|
||||
return l, fmt.Errorf("not a valid severity level: %q", level)
|
||||
}
|
13
pkg/level/severity.go
Normal file
13
pkg/level/severity.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package level
|
||||
|
||||
// Severity represents the severity level of an event.
|
||||
type Severity uint8
|
||||
|
||||
// String returns the severity level as a string, e.g. Alert becomes "alert".
|
||||
func (level Severity) String() string {
|
||||
if b, err := level.MarshalText(); err == nil {
|
||||
return string(b)
|
||||
} else {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package sev
|
||||
package level
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
func TestLevelJsonEncoding(t *testing.T) {
|
||||
type X struct {
|
||||
Level Level
|
||||
Level Severity
|
||||
}
|
||||
|
||||
var x X
|
||||
|
@ -24,7 +24,7 @@ func TestLevelJsonEncoding(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLevelUnmarshalText(t *testing.T) {
|
||||
var u Level
|
||||
var u Severity
|
||||
for _, level := range Levels {
|
||||
t.Run(level.String(), func(t *testing.T) {
|
||||
assert.NoError(t, u.UnmarshalText([]byte(level.String())))
|
||||
|
@ -50,7 +50,7 @@ func TestLevelMarshalText(t *testing.T) {
|
|||
for idx, val := range Levels {
|
||||
level := val
|
||||
t.Run(level.String(), func(t *testing.T) {
|
||||
var cmp Level
|
||||
var cmp Severity
|
||||
b, err := level.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, levelStrings[idx], string(b))
|
|
@ -29,7 +29,7 @@ func AuthToken() string {
|
|||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// IsAuthToken checks if the string might be a valid auth token.
|
||||
// IsAuthToken checks if the string represents a valid auth token.
|
||||
func IsAuthToken(s string) bool {
|
||||
if l := len(s); l == AuthTokenLength {
|
||||
return IsHex(s)
|
||||
|
@ -61,7 +61,7 @@ func AppPassword() string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
// IsAppPassword checks if the string might be a valid app password.
|
||||
// IsAppPassword checks if the string represents a valid app password.
|
||||
func IsAppPassword(s string, verifyChecksum bool) bool {
|
||||
// Verify token length.
|
||||
if len(s) != AppPasswordLength {
|
||||
|
@ -89,7 +89,7 @@ func IsAppPassword(s string, verifyChecksum bool) bool {
|
|||
return s[AppPasswordLength-1] == checksum.Char([]byte(s[:AppPasswordLength-1]))
|
||||
}
|
||||
|
||||
// IsAuthAny checks if the string might be a valid auth token or app password.
|
||||
// IsAuthAny checks if the string represents a valid auth token or app password.
|
||||
func IsAuthAny(s string) bool {
|
||||
// Check if string might be a regular auth token.
|
||||
if IsAuthToken(s) {
|
||||
|
@ -109,7 +109,7 @@ func SessionID(token string) string {
|
|||
return Sha256([]byte(token))
|
||||
}
|
||||
|
||||
// IsSessionID checks if the string is a session id string.
|
||||
// IsSessionID checks if the string represents a valid session id.
|
||||
func IsSessionID(id string) bool {
|
||||
if len(id) != SessionIdLength {
|
||||
return false
|
||||
|
|
19
pkg/rnd/client.go
Normal file
19
pkg/rnd/client.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package rnd
|
||||
|
||||
const (
|
||||
ClientSecretLength = 32
|
||||
)
|
||||
|
||||
// ClientSecret generates a random client secret containing 32 upper and lower case letters as well as numbers.
|
||||
func ClientSecret() string {
|
||||
return Base62(ClientSecretLength)
|
||||
}
|
||||
|
||||
// IsClientSecret checks if the string represents a valid client secret.
|
||||
func IsClientSecret(s string) bool {
|
||||
if l := len(s); l == ClientSecretLength {
|
||||
return IsAlnum(s)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
48
pkg/rnd/client_test.go
Normal file
48
pkg/rnd/client_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientSecret(t *testing.T) {
|
||||
result := ClientSecret()
|
||||
assert.Equal(t, ClientSecretLength, len(result))
|
||||
assert.NotEqual(t, AuthTokenLength, len(result))
|
||||
assert.True(t, IsClientSecret(result))
|
||||
assert.False(t, IsAuthToken(result))
|
||||
assert.False(t, IsHex(result))
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
s := ClientSecret()
|
||||
t.Logf("ClientSecret %d: %s", n, s)
|
||||
assert.True(t, IsClientSecret(s))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkClientSecret(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
ClientSecret()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsClientSecret(t *testing.T) {
|
||||
assert.True(t, IsClientSecret(ClientSecret()))
|
||||
assert.True(t, IsClientSecret("69be27ac5ca305b394046a83f6fda181"))
|
||||
assert.False(t, IsClientSecret("MPkOqm-RtKGOi-ctIvXm-Qv3XhN"))
|
||||
assert.False(t, IsClientSecret("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.False(t, IsClientSecret(AuthToken()))
|
||||
assert.False(t, IsClientSecret(AuthToken()))
|
||||
assert.False(t, IsClientSecret(SessionID(AuthToken())))
|
||||
assert.False(t, IsClientSecret(SessionID(AuthToken())))
|
||||
assert.False(t, IsClientSecret("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.True(t, IsClientSecret("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsClientSecret(""))
|
||||
}
|
||||
|
||||
func BenchmarkIsClientSecret(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
IsClientSecret("69be27ac5ca305b394046a83f6fda181")
|
||||
}
|
||||
}
|
22
pkg/unix/const.go
Normal file
22
pkg/unix/const.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package unix
|
||||
|
||||
// Minute is one minute in seconds.
|
||||
const Minute int64 = 60
|
||||
|
||||
// Hour is one hour in seconds.
|
||||
const Hour = Minute * 60
|
||||
|
||||
// Day is one day in seconds.
|
||||
const Day = Hour * 24
|
||||
|
||||
// Week is one week in seconds.
|
||||
const Week = Day * 7
|
||||
|
||||
// Month is about one month in seconds.
|
||||
const Month = Day * 31
|
||||
|
||||
// Year is 365 days in seconds.
|
||||
const Year = Day * 365
|
||||
|
||||
// YearInt is Year specified as integer.
|
||||
const YearInt = int(Year)
|
8
pkg/unix/time.go
Normal file
8
pkg/unix/time.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package unix
|
||||
|
||||
import "time"
|
||||
|
||||
// Time returns the current time in seconds since January 1, 1970 UTC.
|
||||
func Time() int64 {
|
||||
return time.Now().UTC().Unix()
|
||||
}
|
16
pkg/unix/time_test.go
Normal file
16
pkg/unix/time_test.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package unix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
result := Time()
|
||||
|
||||
assert.Greater(t, result, int64(1706521797))
|
||||
assert.GreaterOrEqual(t, result, time.Now().UTC().Unix())
|
||||
assert.LessOrEqual(t, result, time.Now().UTC().Unix()+2)
|
||||
}
|
25
pkg/unix/unix.go
Normal file
25
pkg/unix/unix.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Package unix provides constants and functions for Unix timestamps.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package unix
|
Loading…
Reference in a new issue