feat: support multiple user groups (#555)
This commit is contained in:
parent
2fcc0a63a0
commit
b742bd89c4
|
|
@ -2,6 +2,7 @@ package acl
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
@ -202,12 +203,21 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
|
|||
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)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -55,8 +55,7 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
|
|||
sid := ""
|
||||
|
||||
if cfg.AuthJwtHeader != "" {
|
||||
// JWTs in the Authorization header are usually prefixed with "Bearer " which is not part of the JWT token.
|
||||
username, usergroup = parseJwt(strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
|
||||
username, usergroup = parseJwtHeader(req)
|
||||
provider = "jwt-header"
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +91,11 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.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 {
|
||||
md, ok := runtime.ServerMetadataFromContext(ctx)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
// "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/MicahParks/keyfunc/v3"
|
||||
|
|
@ -153,7 +154,23 @@ func parseJwt(token string) (string, string) {
|
|||
}
|
||||
|
||||
username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "")
|
||||
usergroup := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUserGroup, "")
|
||||
usergroup := parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,3 +103,58 @@ func TestJWTSignatureVerificationSucceeds(t *testing.T) {
|
|||
func TestJWTSignatureVerificationFails(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue