@@ -8,10 +8,13 @@ import (
88 "io"
99 "io/fs"
1010 "mime"
11+ "net"
1112 "net/http"
13+ "net/netip"
1214 "os"
1315 "path"
1416 "path/filepath"
17+ "strings"
1518 "time"
1619
1720 "github.com/gin-contrib/static"
@@ -30,6 +33,12 @@ var storageDownload = storage.Download
3033
3134var errConfigPathNotFound = errors .New ("config file not found" )
3235
36+ const (
37+ configEditorAuthModeBasic = "basic"
38+ configEditorAuthModeProxy = "proxy"
39+ configEditorAuthModeBasicOrProxy = "basic_or_proxy"
40+ )
41+
3342type embedFileSystem struct {
3443 http.FileSystem
3544 indexes bool
@@ -79,8 +88,8 @@ func StartHTTP(version string) (err error) {
7988
8089 r := setupRouter (version )
8190
82- // Enable baseAuth
83- if len ( config . Web . Username ) > 0 && len ( config . Web . Password ) > 0 {
91+ // Enable BasicAuth for the built-in auth mode.
92+ if configEditorAuthMode () == configEditorAuthModeBasic && hasBasicAuthConfig () {
8493 r .Use (gin .BasicAuth (gin.Accounts {
8594 config .Web .Username : config .Web .Password ,
8695 }))
@@ -121,6 +130,7 @@ func setupRouter(version string) *gin.Engine {
121130 })
122131
123132 group := r .Group ("/api" )
133+ group .Use (requireAPIAuth )
124134 group .GET ("/config" , getConfig )
125135 configGroup := group .Group ("/config" )
126136 configGroup .Use (requireConfigEditorAuth )
@@ -136,25 +146,209 @@ func setupRouter(version string) *gin.Engine {
136146}
137147
138148func requireConfigEditorAuth (c * gin.Context ) {
139- if len (config .Web .Username ) == 0 || len (config .Web .Password ) == 0 {
149+ mode := configEditorAuthMode ()
150+ switch mode {
151+ case configEditorAuthModeBasic :
152+ requireConfigEditorBasicAuth (c )
153+ case configEditorAuthModeProxy :
154+ requireConfigEditorProxyAuth (c )
155+ case configEditorAuthModeBasicOrProxy :
156+ if checkConfigEditorBasicAuth (c ) || checkConfigEditorProxyAuth (c ) {
157+ c .Next ()
158+ return
159+ }
160+ abortConfigEditorAuthRequired (c )
161+ default :
140162 c .AbortWithStatusJSON (http .StatusForbidden , gin.H {
141- "message" : "Config editor requires API authentication." ,
163+ "message" : "Unsupported config editor authentication mode ." ,
142164 })
165+ }
166+ }
167+
168+ func requireAPIAuth (c * gin.Context ) {
169+ mode := configEditorAuthMode ()
170+ switch mode {
171+ case configEditorAuthModeBasic :
172+ c .Next ()
173+ case configEditorAuthModeProxy :
174+ if checkConfigEditorProxyAuth (c ) {
175+ c .Next ()
176+ return
177+ }
178+ abortConfigEditorProxyAuthRequired (c )
179+ case configEditorAuthModeBasicOrProxy :
180+ if checkConfigEditorBasicAuth (c ) || checkConfigEditorProxyAuth (c ) {
181+ c .Next ()
182+ return
183+ }
184+ abortConfigEditorAuthRequired (c )
185+ default :
186+ c .AbortWithStatusJSON (http .StatusForbidden , gin.H {
187+ "message" : "Unsupported config editor authentication mode." ,
188+ })
189+ }
190+ }
191+
192+ func configEditorAuthMode () string {
193+ mode := strings .TrimSpace (config .Web .AuthMode )
194+ if mode == "" {
195+ return configEditorAuthModeBasic
196+ }
197+
198+ return mode
199+ }
200+
201+ func hasBasicAuthConfig () bool {
202+ return config .Web .Username != "" && config .Web .Password != ""
203+ }
204+
205+ func requireConfigEditorBasicAuth (c * gin.Context ) {
206+ if ! hasBasicAuthConfig () {
207+ abortConfigEditorAuthRequired (c )
143208 return
144209 }
145210
211+ if ! checkConfigEditorBasicAuth (c ) {
212+ abortConfigEditorBasicAuthRequired (c )
213+ return
214+ }
215+
216+ c .Next ()
217+ }
218+
219+ func checkConfigEditorBasicAuth (c * gin.Context ) bool {
220+ if ! hasBasicAuthConfig () {
221+ return false
222+ }
223+
146224 username , password , ok := c .Request .BasicAuth ()
147- if ! ok || username != config .Web .Username || password != config .Web .Password {
148- c .Header ("WWW-Authenticate" , `Basic realm="Authorization Required"` )
149- c .AbortWithStatusJSON (http .StatusUnauthorized , gin.H {
150- "message" : "Authentication required." ,
151- })
225+ return ok && username == config .Web .Username && password == config .Web .Password
226+ }
227+
228+ func requireConfigEditorProxyAuth (c * gin.Context ) {
229+ if ! hasProxyAuthConfig () {
230+ abortConfigEditorAuthRequired (c )
231+ return
232+ }
233+
234+ if ! checkConfigEditorProxyAuth (c ) {
235+ abortConfigEditorProxyAuthRequired (c )
152236 return
153237 }
154238
155239 c .Next ()
156240}
157241
242+ func hasProxyAuthConfig () bool {
243+ proxyAuth := config .Web .ProxyAuth
244+ return proxyAuth .UserHeader != "" && len (proxyAuth .TrustedProxies ) > 0 && (len (proxyAuth .AllowedUsers ) > 0 || len (proxyAuth .AllowedGroups ) > 0 )
245+ }
246+
247+ func checkConfigEditorProxyAuth (c * gin.Context ) bool {
248+ if ! hasProxyAuthConfig () || ! requestFromTrustedProxy (c .Request ) {
249+ return false
250+ }
251+
252+ user , ok := singleHeaderValue (c .Request .Header , config .Web .ProxyAuth .UserHeader )
253+ if ! ok {
254+ return false
255+ }
256+
257+ if stringInSlice (user , config .Web .ProxyAuth .AllowedUsers ) {
258+ return true
259+ }
260+
261+ groupHeader := strings .TrimSpace (config .Web .ProxyAuth .GroupHeader )
262+ if groupHeader == "" || len (config .Web .ProxyAuth .AllowedGroups ) == 0 {
263+ return false
264+ }
265+
266+ groupsValue , ok := singleHeaderValue (c .Request .Header , groupHeader )
267+ if ! ok {
268+ return false
269+ }
270+
271+ for _ , group := range strings .Split (groupsValue , "," ) {
272+ if stringInSlice (strings .TrimSpace (group ), config .Web .ProxyAuth .AllowedGroups ) {
273+ return true
274+ }
275+ }
276+
277+ return false
278+ }
279+
280+ func requestFromTrustedProxy (r * http.Request ) bool {
281+ host , _ , err := net .SplitHostPort (r .RemoteAddr )
282+ if err != nil {
283+ host = r .RemoteAddr
284+ }
285+
286+ ip , err := netip .ParseAddr (strings .TrimSpace (host ))
287+ if err != nil {
288+ return false
289+ }
290+
291+ for _ , trustedProxy := range config .Web .ProxyAuth .TrustedProxies {
292+ trustedProxy = strings .TrimSpace (trustedProxy )
293+ if trustedProxy == "" {
294+ continue
295+ }
296+
297+ if prefix , err := netip .ParsePrefix (trustedProxy ); err == nil {
298+ if prefix .Contains (ip ) {
299+ return true
300+ }
301+ continue
302+ }
303+
304+ trustedIP , err := netip .ParseAddr (trustedProxy )
305+ if err == nil && trustedIP == ip {
306+ return true
307+ }
308+ }
309+
310+ return false
311+ }
312+
313+ func singleHeaderValue (header http.Header , name string ) (string , bool ) {
314+ values , ok := header [http .CanonicalHeaderKey (strings .TrimSpace (name ))]
315+ if ! ok || len (values ) != 1 {
316+ return "" , false
317+ }
318+
319+ value := strings .TrimSpace (values [0 ])
320+ return value , value != ""
321+ }
322+
323+ func stringInSlice (value string , values []string ) bool {
324+ for _ , candidate := range values {
325+ if value == strings .TrimSpace (candidate ) {
326+ return true
327+ }
328+ }
329+
330+ return false
331+ }
332+
333+ func abortConfigEditorAuthRequired (c * gin.Context ) {
334+ c .AbortWithStatusJSON (http .StatusForbidden , gin.H {
335+ "message" : "Config editor requires API authentication." ,
336+ })
337+ }
338+
339+ func abortConfigEditorBasicAuthRequired (c * gin.Context ) {
340+ c .Header ("WWW-Authenticate" , `Basic realm="Authorization Required"` )
341+ c .AbortWithStatusJSON (http .StatusUnauthorized , gin.H {
342+ "message" : "Authentication required." ,
343+ })
344+ }
345+
346+ func abortConfigEditorProxyAuthRequired (c * gin.Context ) {
347+ c .AbortWithStatusJSON (http .StatusUnauthorized , gin.H {
348+ "message" : "Proxy authentication required." ,
349+ })
350+ }
351+
158352// GET /api/config
159353func getConfig (c * gin.Context ) {
160354 models := map [string ]any {}
@@ -188,7 +382,6 @@ type configPathStatusEntry struct {
188382 Exists bool `json:"exists"`
189383}
190384
191-
192385func configPathStatuses () []configPathStatus {
193386 knownPaths := config .KnownConfigFilePaths ()
194387 statuses := make ([]configPathStatus , 0 , len (knownPaths ))
0 commit comments