mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
OIDC: Add additional config options and OAuth2 API endpoints #782
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
10
compose.yaml
10
compose.yaml
@@ -45,6 +45,11 @@ services:
|
||||
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
|
||||
PHOTOPRISM_REGISTER_URI: "https://keycloak.localssl.dev/admin/"
|
||||
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
|
||||
## OpenID Connect (pre-configured for local tests):
|
||||
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/auth/realms/master"
|
||||
PHOTOPRISM_OIDC_INSECURE: "true"
|
||||
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
|
||||
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
|
||||
## LDAP Authentication (pre-configured for local tests):
|
||||
PHOTOPRISM_LDAP_URI: "ldap://dummy-ldap:389"
|
||||
PHOTOPRISM_LDAP_INSECURE: "true"
|
||||
@@ -55,11 +60,6 @@ services:
|
||||
PHOTOPRISM_LDAP_ROLE: ""
|
||||
PHOTOPRISM_LDAP_ROLE_DN: "ou=photoprism-*,ou=groups,dc=localssl,dc=dev"
|
||||
PHOTOPRISM_LDAP_WEBDAV_DN: "ou=photoprism-webdav,ou=groups,dc=localssl,dc=dev"
|
||||
## OpenID Connect (pre-configured for local tests):
|
||||
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_INSECURE: "true"
|
||||
## Site Information
|
||||
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
|
||||
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
|
||||
|
||||
@@ -41,6 +41,7 @@ func TestMain(m *testing.M) {
|
||||
// Init test config.
|
||||
c := config.TestConfig()
|
||||
get.SetConfig(c)
|
||||
defer c.CloseDb()
|
||||
|
||||
// Increase login rate limit for testing.
|
||||
limiter.Login = limiter.NewLimit(1, 10000)
|
||||
@@ -48,9 +49,6 @@ func TestMain(m *testing.M) {
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
||||
// Close database connection.
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
|
||||
46
internal/api/oauth_authorize.go
Normal file
46
internal/api/oauth_authorize.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
)
|
||||
|
||||
// OAuthAuthorize is a starting point for browser-based OpenID Connect flows.
|
||||
//
|
||||
// GET /api/v1/oauth/authorize
|
||||
func OAuthAuthorize(router *gin.RouterGroup) {
|
||||
router.GET("/oauth/authorize", func(c *gin.Context) {
|
||||
// Prevent CDNs from caching this endpoint.
|
||||
if header.IsCdn(c.Request) {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIp := ClientIP(c)
|
||||
actor := "unknown client"
|
||||
action := "authorize"
|
||||
|
||||
// Abort if running in public mode.
|
||||
if get.Config().Public() {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.ErrDisabledInPublicMode.Error()})
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
// Send response.
|
||||
c.JSON(http.StatusOK, gin.H{"status": StatusSuccess})
|
||||
})
|
||||
}
|
||||
46
internal/api/oauth_logout.go
Normal file
46
internal/api/oauth_logout.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
)
|
||||
|
||||
// OAuthLogout implements a single logout for OIDC-authenticated clients.
|
||||
//
|
||||
// GET /api/v1/oauth/logout
|
||||
func OAuthLogout(router *gin.RouterGroup) {
|
||||
router.GET("/oauth/logout", func(c *gin.Context) {
|
||||
// Prevent CDNs from caching this endpoint.
|
||||
if header.IsCdn(c.Request) {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIp := ClientIP(c)
|
||||
actor := "unknown client"
|
||||
action := "logout"
|
||||
|
||||
// Abort if running in public mode.
|
||||
if get.Config().Public() {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.ErrDisabledInPublicMode.Error()})
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
// Send response.
|
||||
c.JSON(http.StatusOK, gin.H{"status": StatusSuccess})
|
||||
})
|
||||
}
|
||||
46
internal/api/oauth_redirect.go
Normal file
46
internal/api/oauth_redirect.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
)
|
||||
|
||||
// OAuthRedirect creates a new access token for authenticated clients and then redirects back to the application.
|
||||
//
|
||||
// GET /api/v1/oauth/redirect
|
||||
func OAuthRedirect(router *gin.RouterGroup) {
|
||||
router.GET("/oauth/redirect", func(c *gin.Context) {
|
||||
// Prevent CDNs from caching this endpoint.
|
||||
if header.IsCdn(c.Request) {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Disable caching of responses.
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIp := ClientIP(c)
|
||||
actor := "unknown client"
|
||||
action := "redirect"
|
||||
|
||||
// Abort if running in public mode.
|
||||
if get.Config().Public() {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.ErrDisabledInPublicMode.Error()})
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
// Send response.
|
||||
c.JSON(http.StatusOK, gin.H{"status": StatusSuccess})
|
||||
})
|
||||
}
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// RevokeOAuthToken takes an access token and deletes it. A client may only delete its own tokens.
|
||||
// OAuthRevoke takes an access token and deletes it. A client may only delete its own tokens.
|
||||
//
|
||||
// POST /api/v1/oauth/revoke
|
||||
func RevokeOAuthToken(router *gin.RouterGroup) {
|
||||
func OAuthRevoke(router *gin.RouterGroup) {
|
||||
router.POST("/oauth/revoke", func(c *gin.Context) {
|
||||
// Prevent CDNs from caching this endpoint.
|
||||
if header.IsCdn(c.Request) {
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
@@ -16,9 +14,10 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestRevokeOAuthToken(t *testing.T) {
|
||||
func TestOAuthRevoke(t *testing.T) {
|
||||
const tokenPath = "/api/v1/oauth/token"
|
||||
const revokePath = "/api/v1/oauth/revoke"
|
||||
|
||||
@@ -27,8 +26,8 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
RevokeOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
data := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
@@ -63,8 +62,8 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
RevokeOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
createData := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
@@ -104,8 +103,8 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
RevokeOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
createData := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
@@ -145,8 +144,8 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
RevokeOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
createData := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
@@ -186,8 +185,8 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
RevokeOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
createData := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
@@ -225,7 +224,7 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
t.Run("PublicMode", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
RevokeOAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
sess := entity.SessionFixtures.Get("alice_token")
|
||||
|
||||
@@ -246,8 +245,8 @@ func TestRevokeOAuthToken(t *testing.T) {
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
CreateOAuthToken(router)
|
||||
RevokeOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
OAuthRevoke(router)
|
||||
|
||||
data := url.Values{
|
||||
"grant_type": {"password"},
|
||||
@@ -18,11 +18,10 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// CreateOAuthToken creates a new access token for clients that
|
||||
// authenticate with valid OAuth2 client credentials.
|
||||
// OAuthToken creates a new access token for clients that authenticate with valid OAuth2 client credentials.
|
||||
//
|
||||
// POST /api/v1/oauth/token
|
||||
func CreateOAuthToken(router *gin.RouterGroup) {
|
||||
func OAuthToken(router *gin.RouterGroup) {
|
||||
router.POST("/oauth/token", func(c *gin.Context) {
|
||||
// Prevent CDNs from caching this endpoint.
|
||||
if header.IsCdn(c.Request) {
|
||||
@@ -7,19 +7,20 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateOAuthToken(t *testing.T) {
|
||||
func TestOAuthToken(t *testing.T) {
|
||||
t.Run("ClientSuccess", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -44,7 +45,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
t.Run("PublicMode", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -71,7 +72,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -98,7 +99,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -124,7 +125,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -151,7 +152,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -178,7 +179,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -204,7 +205,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -234,7 +235,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -263,7 +264,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -294,7 +295,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -325,7 +326,7 @@ func TestCreateOAuthToken(t *testing.T) {
|
||||
|
||||
sessId := AuthenticateUser(app, router, "deleted", "Deleted123!")
|
||||
|
||||
CreateOAuthToken(router)
|
||||
OAuthToken(router)
|
||||
|
||||
var method = "POST"
|
||||
var path = "/api/v1/oauth/token"
|
||||
@@ -16,8 +16,12 @@ func TestMain(m *testing.M) {
|
||||
c := config.TestConfig()
|
||||
defer c.CloseDb()
|
||||
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
||||
// Close database connection.
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,13 @@ func TestMain(m *testing.M) {
|
||||
|
||||
c := config.NewTestConfig("commands")
|
||||
get.SetConfig(c)
|
||||
defer c.CloseDb()
|
||||
|
||||
InitConfig = func(ctx *cli.Context) (*config.Config, error) {
|
||||
return c, c.Init()
|
||||
}
|
||||
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
||||
os.Exit(code)
|
||||
|
||||
@@ -25,7 +25,7 @@ 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) {
|
||||
{Title: "OpenID Connect (OIDC)", NoWrap: true, Report: func(conf *config.Config) ([][]string, []string) {
|
||||
return conf.OIDCReport()
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -3,24 +3,21 @@ package commands
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
)
|
||||
|
||||
func TestShowConfigOptionsCommand(t *testing.T) {
|
||||
var err error
|
||||
|
||||
ctx := config.CliTestContext()
|
||||
ctx := NewTestContext(nil)
|
||||
|
||||
output := capture.Output(func() {
|
||||
err = ShowConfigOptionsCommand.Run(ctx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "PHOTOPRISM_IMPORT_PATH")
|
||||
assert.Contains(t, output, "--sidecar-path")
|
||||
assert.Contains(t, output, "sidecar `PATH` *optional*")
|
||||
|
||||
@@ -11,26 +11,26 @@ 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 != ""
|
||||
return c.options.OIDCUri != "" && c.options.OIDCClient != "" && c.options.OIDCSecret != ""
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
// OIDCUri returns the OpenID Connect issuer URI as *url.URL for single sign-on via OIDC.
|
||||
func (c *Config) OIDCUri() *url.URL {
|
||||
if uri := c.options.OIDCUri; uri == "" {
|
||||
return &url.URL{}
|
||||
} else if result, err := url.Parse(oidcIssuer); err != nil {
|
||||
log.Errorf("oidc: failed to parse issuer URL (%s)", err)
|
||||
} else if result, err := url.Parse(uri); err != nil {
|
||||
log.Errorf("oidc: failed to parse issuer URI (%s)", err)
|
||||
return &url.URL{}
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// OIDCInsecure checks if OIDC issuer SSL/TLS certificate verification should be skipped.
|
||||
func (c *Config) OIDCInsecure() bool {
|
||||
return c.options.OIDCInsecure
|
||||
}
|
||||
|
||||
// OIDCClient returns the Client ID for single sign-on via OIDC.
|
||||
func (c *Config) OIDCClient() string {
|
||||
return c.options.OIDCClient
|
||||
@@ -50,9 +50,14 @@ func (c *Config) OIDCScopes() string {
|
||||
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
|
||||
// OIDCIcon returns the custom issuer icon URI for single sign-on via OIDC, if any.
|
||||
func (c *Config) OIDCIcon() string {
|
||||
return c.options.OIDCIcon
|
||||
}
|
||||
|
||||
// OIDCButton returns the custom button text for single sign-on via OIDC, if any.
|
||||
func (c *Config) OIDCButton() string {
|
||||
return c.options.OIDCButton
|
||||
}
|
||||
|
||||
// OIDCRegister checks if new accounts may be created via OIDC.
|
||||
@@ -60,17 +65,25 @@ func (c *Config) OIDCRegister() bool {
|
||||
return c.options.OIDCRegister
|
||||
}
|
||||
|
||||
// OIDCRedirect checks if unauthenticated users should automatically be redirected to the OIDC login page.
|
||||
func (c *Config) OIDCRedirect() bool {
|
||||
return c.options.OIDCRedirect
|
||||
}
|
||||
|
||||
// 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-uri", c.OIDCUri().String()},
|
||||
{"oidc-insecure", fmt.Sprintf("%t", c.OIDCInsecure())},
|
||||
{"oidc-client", c.OIDCClient()},
|
||||
{"oidc-secret", strings.Repeat("*", utf8.RuneCountInString(c.OIDCSecret()))},
|
||||
{"oidc-scopes", c.OIDCScopes()},
|
||||
{"oidc-insecure", fmt.Sprintf("%t", c.OIDCInsecure())},
|
||||
{"oidc-icon", c.OIDCIcon()},
|
||||
{"oidc-button", c.OIDCButton()},
|
||||
{"oidc-register", fmt.Sprintf("%t", c.OIDCRegister())},
|
||||
{"oidc-redirect", fmt.Sprintf("%t", c.OIDCRedirect())},
|
||||
}
|
||||
|
||||
return rows, cols
|
||||
|
||||
@@ -13,20 +13,24 @@ func TestConfig_OIDCEnabled(t *testing.T) {
|
||||
assert.False(t, c.OIDCEnabled())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCIssuer(t *testing.T) {
|
||||
func TestConfig_OIDCUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.OIDCIssuer())
|
||||
assert.IsType(t, &url.URL{}, c.OIDCUri())
|
||||
assert.Equal(t, "", c.OIDCUri().Path)
|
||||
|
||||
c.options.OIDCUri = "test"
|
||||
assert.Equal(t, "test", c.OIDCUri().Path)
|
||||
c.options.OIDCUri = ""
|
||||
|
||||
assert.IsType(t, &url.URL{}, c.OIDCUri())
|
||||
assert.Equal(t, "", c.OIDCUri().String())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCIssuerURL(t *testing.T) {
|
||||
func TestConfig_OIDCInsecure(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.IsType(t, &url.URL{}, c.OIDCIssuerURL())
|
||||
assert.Equal(t, "", c.OIDCIssuerURL().Path)
|
||||
|
||||
c.options.OIDCIssuer = "test"
|
||||
assert.Equal(t, "test", c.OIDCIssuerURL().Path)
|
||||
assert.False(t, c.OIDCInsecure())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCClient(t *testing.T) {
|
||||
@@ -51,16 +55,28 @@ func TestConfig_OIDCScopes(t *testing.T) {
|
||||
assert.Equal(t, OIDCDefaultScopes, c.OIDCScopes())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCIcon(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.OIDCIcon())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCButton(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.OIDCButton())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCRegister(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.False(t, c.OIDCRegister())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCInsecure(t *testing.T) {
|
||||
func TestConfig_OIDCRedirect(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.False(t, c.OIDCInsecure())
|
||||
assert.False(t, c.OIDCRedirect())
|
||||
}
|
||||
|
||||
func TestConfig_OIDCReport(t *testing.T) {
|
||||
|
||||
@@ -20,11 +20,10 @@ func TestMain(m *testing.M) {
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
|
||||
c := TestConfig()
|
||||
defer c.CloseDb()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,10 +44,15 @@ var Flags = CliFlags{
|
||||
EnvVar: EnvVar("ADMIN_PASSWORD"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "oidc-issuer",
|
||||
Usage: "server `URI` for single sign-on via OpenID Connect (OIDC)",
|
||||
Name: "oidc-uri",
|
||||
Usage: "issuer `URI` for single sign-on via OpenID Connect (OIDC), e.g. https://accounts.google.com/o/oauth2/v2/auth",
|
||||
Value: "",
|
||||
EnvVar: EnvVar("OIDC_ISSUER"),
|
||||
EnvVar: EnvVar("OIDC_URI"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "oidc-insecure",
|
||||
Usage: "skip issuer SSL/TLS certificate verification",
|
||||
EnvVar: EnvVar("OIDC_INSECURE"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "oidc-client",
|
||||
@@ -68,16 +73,28 @@ var Flags = CliFlags{
|
||||
Value: OIDCDefaultScopes,
|
||||
EnvVar: EnvVar("OIDC_SCOPES"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "oidc-insecure",
|
||||
Usage: "skip issuer SSL/TLS certificate verification",
|
||||
EnvVar: EnvVar("OIDC_INSECURE"),
|
||||
Flag: cli.StringFlag{
|
||||
Name: "oidc-icon",
|
||||
Usage: "custom issuer icon `URI` for single sign-on via OIDC",
|
||||
Value: "",
|
||||
EnvVar: EnvVar("OIDC_ICON"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "oidc-button",
|
||||
Usage: "custom login button `TEXT` for single sign-on via OIDC",
|
||||
Value: "",
|
||||
EnvVar: EnvVar("OIDC_BUTTON"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "oidc-register",
|
||||
Usage: "allow user registration via OIDC",
|
||||
EnvVar: EnvVar("OIDC_REGISTER"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "oidc-redirect",
|
||||
Usage: "automatically redirect unauthenticated users to the OIDC login page",
|
||||
EnvVar: EnvVar("OIDC_REDIRECT"),
|
||||
}}, {
|
||||
Flag: cli.Int64Flag{
|
||||
Name: "session-maxage",
|
||||
Value: DefaultSessionMaxAge,
|
||||
|
||||
@@ -31,12 +31,15 @@ type Options struct {
|
||||
PasswordResetUri string `yaml:"PasswordResetUri" json:"-" flag:"password-reset-uri"`
|
||||
RegisterUri string `yaml:"RegisterUri" json:"-" flag:"register-uri"`
|
||||
LoginUri string `yaml:"LoginUri" json:"-" flag:"login-uri"`
|
||||
OIDCIssuer string `yaml:"OIDCIssuer" json:"OIDCIssuer" flag:"oidc-issuer"`
|
||||
OIDCUri string `yaml:"OIDCUri" json:"OIDCUri" flag:"oidc-uri"`
|
||||
OIDCInsecure bool `yaml:"OIDCInsecure" json:"-" flag:"oidc-insecure"`
|
||||
OIDCClient string `yaml:"OIDCClient" json:"-" flag:"oidc-client"`
|
||||
OIDCSecret string `yaml:"OIDCSecret" json:"-" flag:"oidc-secret"`
|
||||
OIDCScopes string `yaml:"OIDCScopes" json:"-" flag:"oidc-scopes"`
|
||||
OIDCInsecure bool `yaml:"OIDCInsecure" json:"-" flag:"oidc-insecure"`
|
||||
OIDCIcon string `yaml:"OIDCIcon" json:"-" flag:"oidc-icon"`
|
||||
OIDCButton string `yaml:"OIDCButton" json:"-" flag:"oidc-button"`
|
||||
OIDCRegister bool `yaml:"OIDCRegister" json:"-" flag:"oidc-register"`
|
||||
OIDCRedirect bool `yaml:"OIDCRedirect" json:"-" flag:"oidc-redirect"`
|
||||
SessionMaxAge int64 `yaml:"SessionMaxAge" json:"-" flag:"session-maxage"`
|
||||
SessionTimeout int64 `yaml:"SessionTimeout" json:"-" flag:"session-timeout"`
|
||||
SessionCache int64 `yaml:"SessionCache" json:"-" flag:"session-cache"`
|
||||
|
||||
@@ -229,7 +229,7 @@ func CliTestContext() *cli.Context {
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.String("config-path", config.ConfigPath, "doc")
|
||||
globalSet.String("admin-password", config.DarktableBin, "doc")
|
||||
globalSet.String("oidc-issuer", config.OIDCIssuer, "doc")
|
||||
globalSet.String("oidc-uri", config.OIDCUri, "doc")
|
||||
globalSet.String("oidc-client", config.OIDCClient, "doc")
|
||||
globalSet.String("oidc-secret", config.OIDCSecret, "doc")
|
||||
globalSet.String("oidc-scopes", config.OIDCScopes, "doc")
|
||||
@@ -263,7 +263,7 @@ func CliTestContext() *cli.Context {
|
||||
|
||||
LogErr(c.Set("config-path", config.ConfigPath))
|
||||
LogErr(c.Set("admin-password", config.AdminPassword))
|
||||
LogErr(c.Set("oidc-issuer", config.OIDCIssuer))
|
||||
LogErr(c.Set("oidc-uri", config.OIDCUri))
|
||||
LogErr(c.Set("oidc-client", config.OIDCClient))
|
||||
LogErr(c.Set("oidc-secret", config.OIDCSecret))
|
||||
LogErr(c.Set("oidc-scopes", OIDCDefaultScopes))
|
||||
|
||||
@@ -10,7 +10,7 @@ var onceOidc sync.Once
|
||||
|
||||
func initOidc() {
|
||||
services.OIDC, _ = oidc.NewClient(
|
||||
Config().OIDCIssuerURL(),
|
||||
Config().OIDCUri(),
|
||||
Config().OIDCClient(),
|
||||
Config().OIDCSecret(),
|
||||
Config().OIDCScopes(),
|
||||
@@ -20,7 +20,7 @@ func initOidc() {
|
||||
}
|
||||
|
||||
func OIDC() *oidc.Client {
|
||||
oncePhotos.Do(initOidc)
|
||||
onceOidc.Do(initOidc)
|
||||
|
||||
return services.OIDC
|
||||
}
|
||||
|
||||
@@ -14,10 +14,9 @@ func TestMain(m *testing.M) {
|
||||
|
||||
c := config.NewTestConfig("photoprism")
|
||||
SetConfig(c)
|
||||
defer c.CloseDb()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,11 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||
api.CreateSession(APIv1)
|
||||
api.GetSession(APIv1)
|
||||
api.DeleteSession(APIv1)
|
||||
api.CreateOAuthToken(APIv1)
|
||||
api.RevokeOAuthToken(APIv1)
|
||||
api.OAuthAuthorize(APIv1)
|
||||
api.OAuthRedirect(APIv1)
|
||||
api.OAuthToken(APIv1)
|
||||
api.OAuthRevoke(APIv1)
|
||||
api.OAuthLogout(APIv1)
|
||||
|
||||
// Server Config.
|
||||
api.GetConfigOptions(APIv1)
|
||||
|
||||
@@ -21,6 +21,7 @@ func TestMain(m *testing.M) {
|
||||
// Init test config.
|
||||
c := config.TestConfig()
|
||||
get.SetConfig(c)
|
||||
defer c.CloseDb()
|
||||
|
||||
// Increase login rate limit for testing.
|
||||
limiter.Login = limiter.NewLimit(1, 10000)
|
||||
@@ -28,8 +29,5 @@ func TestMain(m *testing.M) {
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
||||
// Close database connection.
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ func TestMain(m *testing.M) {
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
event.AuditLog = log
|
||||
|
||||
defer Shutdown()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Remove generated test files and folders.
|
||||
@@ -22,8 +24,6 @@ func TestMain(m *testing.M) {
|
||||
_ = os.RemoveAll("testdata/cache")
|
||||
_ = os.RemoveAll("testdata/vips")
|
||||
|
||||
Shutdown()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user