Merge branch 'main' of ssh://github.com/OliveTin/OliveTin

This commit is contained in:
jamesread 2025-04-08 21:49:22 +01:00
commit c5eaa35fb0
5 changed files with 128 additions and 5 deletions

View File

@ -2,6 +2,7 @@ package acl
import ( import (
"context" "context"
"strings"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -202,14 +203,23 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
continue continue
} }
if slices.Contains(acl.MatchUsergroups, user.Usergroup) { // handle multiple usergroups - groups will be separated by a space
if hasGroupsMatch(acl.MatchUsergroups, user.Usergroup) {
user.Acls = append(user.Acls, acl.Name) user.Acls = append(user.Acls, acl.Name)
continue continue
} }
} }
} }
func hasGroupsMatch(matchUsergroups []string, usergroup string) bool {
for _, group := range strings.Fields(usergroup) {
if slices.Contains(matchUsergroups, group) {
return true
}
}
return false
}
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool { func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
if !slices.Contains(user.Acls, acl.Name) { if !slices.Contains(user.Acls, acl.Name) {
// If the user does not have this ACL, then it is not relevant // If the user does not have this ACL, then it is not relevant

View File

@ -0,0 +1,37 @@
package acl
import "testing"
func Test_hasGroupsMatch(t *testing.T) {
tests := []struct {
name string
matchUsergroups []string
usergroup string
want bool
}{
{
name: "No groups match",
matchUsergroups: []string{"group1", "group2"},
usergroup: "group3",
},
{
name: "Exact match",
matchUsergroups: []string{"group1", "group2"},
usergroup: "group1",
want: true,
},
{
name: "Multiple groups match",
matchUsergroups: []string{"group1", "group2"},
usergroup: "group1 group2",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasGroupsMatch(tt.matchUsergroups, tt.usergroup); got != tt.want {
t.Errorf("hasGroupsMatch() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -55,8 +55,7 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
sid := "" sid := ""
if cfg.AuthJwtHeader != "" { if cfg.AuthJwtHeader != "" {
// JWTs in the Authorization header are usually prefixed with "Bearer " which is not part of the JWT token. username, usergroup = parseJwtHeader(req)
username, usergroup = parseJwt(strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
provider = "jwt-header" provider = "jwt-header"
} }
@ -92,6 +91,11 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
return md return md
} }
func parseJwtHeader(req *http.Request) (string, string) {
// JWTs in the Authorization header are usually prefixed with "Bearer " which is not part of the JWT token.
return parseJwt(strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
}
func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error { func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
md, ok := runtime.ServerMetadataFromContext(ctx) md, ok := runtime.ServerMetadataFromContext(ctx)

View File

@ -9,6 +9,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http" "net/http"
"os" "os"
"strings"
// "github.com/coreos/go-oidc/v3/oidc" // "github.com/coreos/go-oidc/v3/oidc"
"github.com/MicahParks/keyfunc/v3" "github.com/MicahParks/keyfunc/v3"
@ -153,7 +154,23 @@ func parseJwt(token string) (string, string) {
} }
username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "") username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "")
usergroup := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUserGroup, "") usergroup := parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims)
return username, usergroup return username, usergroup
} }
func parseGroupClaim(groupClaim string, claims jwt.MapClaims) string {
usergroup := ""
if val, ok := claims[groupClaim]; ok {
if array, ok := val.([]interface{}); ok {
groups := make([]string, len(array))
for i, v := range array {
groups[i] = fmt.Sprintf("%s", v)
}
usergroup = strings.Join(groups, " ")
} else {
usergroup = fmt.Sprintf("%s", val)
}
}
return usergroup
}

View File

@ -103,3 +103,58 @@ func TestJWTSignatureVerificationSucceeds(t *testing.T) {
func TestJWTSignatureVerificationFails(t *testing.T) { func TestJWTSignatureVerificationFails(t *testing.T) {
testJwkValidation(t, -500, 403) testJwkValidation(t, -500, 403)
} }
func TestJWTHeader(t *testing.T) {
privateKey, publicKeyPath := createKeys(t)
defer os.Remove(publicKeyPath)
cfg := config.DefaultConfig()
cfg.AuthJwtPubKeyPath = publicKeyPath
cfg.AuthJwtClaimUsername = "sub"
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
cfg.AuthJwtHeader = "Authorization"
SetGlobalRestConfig(cfg) // ugly, setting global var, we should pass configs as params to modules... :/
token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims)
claims["nbf"] = time.Now().Unix() - 1000
claims["exp"] = time.Now().Unix() + 2000
claims["sub"] = "test"
claims["olivetinGroup"] = []string{"test", "test2"}
tokenStr, _ := token.SignedString(privateKey)
mux := newMux()
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
username, usergroup := parseJwtHeader(r)
if username == "" {
w.WriteHeader(403)
}
assert.Equal(t, "test", username)
assert.Equal(t, "test test2", usergroup)
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
})
srv := setupTestingServer(mux, t)
req, client := newReq("")
req.Header.Set("Authorization", "Bearer "+tokenStr)
res, err := client.Do(req)
if err != nil {
t.Fatalf("Client err: %+v", err)
} else {
defer res.Body.Close()
assert.Equal(t, 200, res.StatusCode)
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
srv.Shutdown(context.TODO())
}