Config: Refactor OIDC options and report #782

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2024-06-25 10:07:01 +02:00
parent 8c67fb1fe8
commit a436dc3fd8
12 changed files with 121 additions and 22 deletions

View file

@ -59,7 +59,6 @@ services:
PHOTOPRISM_OIDC_ISSUER: "https://keycloak.localssl.dev/auth/realms/master"
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
PHOTOPRISM_OIDC_SCOPES: "openid profile"
PHOTOPRISM_OIDC_INSECURE: "true"
## Site Information
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"

View file

@ -25,6 +25,9 @@ var ConfigReports = []Report{
{Title: "Global Config Options", NoWrap: true, Report: func(conf *config.Config) ([][]string, []string) {
return conf.Report()
}},
{Title: "OpenID Connect", NoWrap: true, Report: func(conf *config.Config) ([][]string, []string) {
return conf.OIDCReport()
}},
}
// showConfigAction shows global config option names and values.

View file

@ -69,7 +69,7 @@ func (f CliFlags) Replace(name string, replacement CliFlag) CliFlags {
return f
}
// Insert inserts command flags, if possible after name.
// Insert inserts command flags, if possible after the flag specified by name.
func (f CliFlags) Insert(name string, insert []CliFlag) (result CliFlags) {
result = make(CliFlags, 0, len(f)+len(insert))
@ -92,6 +92,29 @@ func (f CliFlags) Insert(name string, insert []CliFlag) (result CliFlags) {
return result
}
// InsertBefore inserts command flags, if possible before the flag specified by name.
func (f CliFlags) InsertBefore(name string, insert []CliFlag) (result CliFlags) {
result = make(CliFlags, 0, len(f)+len(insert))
done := false
for _, flag := range f {
if !done && flag.Name() == name {
result = append(result, insert...)
done = true
}
result = append(result, flag)
}
if !done {
log.Warnf("config: failed to insert cli flags before %s", clean.Log(name))
result = append(result, insert...)
}
return result
}
// Prepend adds command flags at the beginning.
func (f CliFlags) Prepend(el []CliFlag) (result CliFlags) {
result = make(CliFlags, 0, len(f)+len(el))

View file

@ -1,17 +1,36 @@
package config
const OIDCDefaultScopes = "openid profile"
import (
"fmt"
"net/url"
"strings"
"unicode/utf8"
)
const OIDCDefaultScopes = "openid email profile"
// OIDCEnabled checks if login via OpenID Connect (OIDC) is enabled.
func (c *Config) OIDCEnabled() bool {
return c.options.OIDCIssuer != "" && c.options.OIDCClient != "" && c.options.OIDCSecret != ""
}
// OIDCIssuer returns the OpenID Connect Issuer URL for single sign-on via OIDC.
// OIDCIssuer returns the OpenID Connect Issuer URL as string for single sign-on via OIDC.
func (c *Config) OIDCIssuer() string {
return c.options.OIDCIssuer
}
// OIDCIssuerURL returns the OpenID Connect Issuer URL as *url.URL for single sign-on via OIDC.
func (c *Config) OIDCIssuerURL() *url.URL {
if oidcIssuer := c.OIDCIssuer(); oidcIssuer == "" {
return &url.URL{}
} else if result, err := url.Parse(oidcIssuer); err != nil {
log.Errorf("oidc: failed to parse issuer URL (%s)", err)
return &url.URL{}
} else {
return result
}
}
// OIDCClient returns the Client ID for single sign-on via OIDC.
func (c *Config) OIDCClient() string {
return c.options.OIDCClient
@ -22,17 +41,37 @@ func (c *Config) OIDCSecret() string {
return c.options.OIDCSecret
}
// OIDCScopes returns the token request scopes for single sign-on via OIDC.
// OIDCScopes returns the user information scopes for single sign-on via OIDC.
func (c *Config) OIDCScopes() string {
if c.options.OIDCScopes == "" {
return OIDCDefaultScopes
}
return c.options.OIDCScopes
}
// OIDCInsecure checks if OIDC issuer SSL/TLS certificate verification should be skipped.
func (c *Config) OIDCInsecure() bool {
return c.options.OIDCInsecure
}
// OIDCRegister checks if new accounts may be created via OIDC.
func (c *Config) OIDCRegister() bool {
return c.options.OIDCRegister
}
// OIDCInsecure checks if OIDC issuer SSL/TLS certificate verification should be skipped.
func (c *Config) OIDCInsecure() bool {
return c.options.OIDCInsecure
// OIDCReport returns the OpenID Connect config values as a table for reporting.
func (c *Config) OIDCReport() (rows [][]string, cols []string) {
cols = []string{"Name", "Value"}
rows = [][]string{
{"oidc-issuer", c.OIDCIssuer()},
{"oidc-client", c.OIDCClient()},
{"oidc-secret", strings.Repeat("*", utf8.RuneCountInString(c.OIDCSecret()))},
{"oidc-scopes", c.OIDCScopes()},
{"oidc-insecure", fmt.Sprintf("%t", c.OIDCInsecure())},
{"oidc-register", fmt.Sprintf("%t", c.OIDCRegister())},
}
return rows, cols
}

View file

@ -1,6 +1,7 @@
package config
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
@ -18,6 +19,12 @@ func TestConfig_OIDCIssuer(t *testing.T) {
assert.Equal(t, "", c.OIDCIssuer())
}
func TestConfig_OIDCIssuerURL(t *testing.T) {
c := NewConfig(CliTestContext())
assert.IsType(t, &url.URL{}, c.OIDCIssuerURL())
}
func TestConfig_OIDCClient(t *testing.T) {
c := NewConfig(CliTestContext())
@ -33,7 +40,7 @@ func TestConfig_OIDCSecret(t *testing.T) {
func TestConfig_OIDCScopes(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "openid profile", c.OIDCScopes())
assert.Equal(t, OIDCDefaultScopes, c.OIDCScopes())
}
func TestConfig_OIDCRegister(t *testing.T) {

View file

@ -63,20 +63,21 @@ var Flags = CliFlags{
}}, {
Flag: cli.StringFlag{
Name: "oidc-scopes",
Hidden: true,
Usage: "user information `SCOPES` for single sign-on via OIDC",
Value: OIDCDefaultScopes,
EnvVar: EnvVar("OIDC_SCOPES"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-register",
Usage: "allow creating new accounts via OIDC",
EnvVar: EnvVar("OIDC_REGISTER"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-insecure",
Usage: "skip issuer SSL/TLS certificate verification",
EnvVar: EnvVar("OIDC_INSECURE"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-register",
Usage: "allow user registration via OIDC",
EnvVar: EnvVar("OIDC_REGISTER"),
}}, {
Flag: cli.Int64Flag{
Name: "session-maxage",
Value: DefaultSessionMaxAge,

View file

@ -35,8 +35,8 @@ type Options struct {
OIDCClient string `yaml:"OIDCClient" json:"-" flag:"oidc-client"`
OIDCSecret string `yaml:"OIDCSecret" json:"-" flag:"oidc-secret"`
OIDCScopes string `yaml:"OIDCScopes" json:"-" flag:"oidc-scopes"`
OIDCRegister bool `yaml:"OIDCRegister" json:"-" flag:"oidc-register"`
OIDCInsecure bool `yaml:"OIDCInsecure" json:"-" flag:"oidc-insecure"`
OIDCRegister bool `yaml:"OIDCRegister" json:"-" flag:"oidc-register"`
SessionMaxAge int64 `yaml:"SessionMaxAge" json:"-" flag:"session-maxage"`
SessionTimeout int64 `yaml:"SessionTimeout" json:"-" flag:"session-timeout"`
SessionCache int64 `yaml:"SessionCache" json:"-" flag:"session-cache"`

View file

@ -28,12 +28,6 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"password-reset-uri", c.PasswordResetUri()},
{"register-uri", c.RegisterUri()},
{"login-uri", c.LoginUri()},
{"oidc-issuer", c.OIDCIssuer()},
{"oidc-client", c.OIDCClient()},
{"oidc-secret", c.OIDCSecret()},
{"oidc-scopes", c.OIDCScopes()},
{"oidc-register", fmt.Sprintf("%t", c.OIDCRegister())},
{"oidc-insecure", fmt.Sprintf("%t", c.OIDCInsecure())},
{"session-maxage", fmt.Sprintf("%d", c.SessionMaxAge())},
{"session-timeout", fmt.Sprintf("%d", c.SessionTimeout())},
{"session-cache", fmt.Sprintf("%d", c.SessionCache())},

26
internal/get/oidc.go Normal file
View file

@ -0,0 +1,26 @@
package get
import (
"sync"
"github.com/photoprism/photoprism/internal/oidc"
)
var onceOidc sync.Once
func initOidc() {
services.OIDC, _ = oidc.NewClient(
Config().OIDCIssuerURL(),
Config().OIDCClient(),
Config().OIDCSecret(),
Config().OIDCScopes(),
Config().SiteUrl(),
Config().Debug(),
)
}
func OIDC() *oidc.Client {
oncePhotos.Do(initOidc)
return services.OIDC
}

View file

@ -29,6 +29,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/oidc"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/session"
@ -58,6 +59,7 @@ var services struct {
Query *query.Query
Thumbs *photoprism.Thumbs
Session *session.Session
OIDC *oidc.Client
}
func SetConfig(c *config.Config) {

View file

@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/oidc"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/session"
@ -72,3 +73,7 @@ func TestResample(t *testing.T) {
func TestSession(t *testing.T) {
assert.IsType(t, &session.Session{}, Session())
}
func TestOIDC(t *testing.T) {
assert.IsType(t, &oidc.Client{}, OIDC())
}

View file

@ -87,7 +87,7 @@ func NewClient(iss *url.URL, clientId, clientSecret, customScopes, siteUrl strin
}
}
scopes := strings.Split(strings.TrimSpace("openid profile email "+customScopes), " ")
scopes := strings.Split(strings.TrimSpace("openid email profile "+customScopes), " ")
provider, err := rp.NewRelyingPartyOIDC(iss.String(), clientId, clientSecret, u.String(), scopes, clientOpt...)