From 4f06568f8ae93e36b4a7f72e9549cf3729bbc583 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 5 Jul 2024 12:06:25 +0200 Subject: [PATCH] Auth: Add support for escaping colon characters in org_mapping (#89951) * Split org_mapping correctly if it contains multiple colons * Improve tests * Use backslash as an escape character for colons * Cleanup, address feedback * Change test to use double quotes as an example --- .../social/connectors/org_role_mapper.go | 33 +++++++++++++++++-- .../social/connectors/org_role_mapper_test.go | 33 ++++++++++++++++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/pkg/login/social/connectors/org_role_mapper.go b/pkg/login/social/connectors/org_role_mapper.go index bb3d7974cb7..af08ee7a514 100644 --- a/pkg/login/social/connectors/org_role_mapper.go +++ b/pkg/login/social/connectors/org_role_mapper.go @@ -3,6 +3,7 @@ package connectors import ( "context" "fmt" + "regexp" "strconv" "strings" @@ -11,7 +12,12 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -const mapperMatchAllOrgID = -1 +const ( + mapperMatchAllOrgID = -1 + escapeStr = `\` +) + +var separatorRegexp = regexp.MustCompile(":") // OrgRoleMapper maps external orgs/groups to Grafana orgs and basic roles. type OrgRoleMapper struct { @@ -132,7 +138,7 @@ func (m *OrgRoleMapper) ParseOrgMappingSettings(ctx context.Context, mappings [] res := map[string]map[int64]org.RoleType{} for _, v := range mappings { - kv := strings.Split(v, ":") + kv := splitOrgMapping(v) if !isValidOrgMappingFormat(kv) { m.logger.Error("Skipping org mapping due to invalid format.", "mapping", fmt.Sprintf("%v", v)) if roleStrict { @@ -203,6 +209,29 @@ func (m *OrgRoleMapper) getAllOrgs() (map[int64]bool, error) { return allOrgIDs, nil } +func splitOrgMapping(mapping string) []string { + result := make([]string, 0, 3) + matches := separatorRegexp.FindAllStringIndex(mapping, -1) + from := 0 + + for _, match := range matches { + // match[0] is the start, match[1] is the end of the match + start, end := match[0], match[1] + // Check if the match is not preceded by two backslashes + if start == 0 || mapping[start-1:start] != escapeStr { + result = append(result, strings.ReplaceAll(mapping[from:end-1], escapeStr, "")) + from = end + } + } + + result = append(result, mapping[from:]) + if len(result) > 3 { + return []string{} + } + + return result +} + func getRoleForInternalOrgMapping(kv []string) org.RoleType { if len(kv) > 2 && org.RoleType(kv[2]).IsValid() { return org.RoleType(kv[2]) diff --git a/pkg/login/social/connectors/org_role_mapper_test.go b/pkg/login/social/connectors/org_role_mapper_test.go index 75aebe596be..3c9760e5d4a 100644 --- a/pkg/login/social/connectors/org_role_mapper_test.go +++ b/pkg/login/social/connectors/org_role_mapper_test.go @@ -268,6 +268,29 @@ func TestOrgRoleMapper_ParseOrgMappingSettings(t *testing.T) { strictRoleMapping: false, }, }, + { + name: "should return correct mapping when the first part contains multiple colons", + rawMapping: []string{"Groups\\:IT\\:ops:1:Viewer"}, + roleStrict: false, + expected: &MappingConfiguration{ + orgMapping: map[string]map[int64]org.RoleType{"Groups:IT:ops": {1: org.RoleViewer}}, + strictRoleMapping: false, + }, + }, + { + name: "should return correct mapping when the org name contains multiple colons", + rawMapping: []string{`Group1:Org\:1:Viewer`}, + roleStrict: false, + setupMock: func(orgService *orgtest.MockService) { + orgService.On("GetByName", mock.Anything, mock.MatchedBy(func(query *org.GetOrgByNameQuery) bool { + return query.Name == "Org:1" + })).Return(&org.Org{ID: 1}, nil) + }, + expected: &MappingConfiguration{ + orgMapping: map[string]map[int64]org.RoleType{"Group1": {1: org.RoleViewer}}, + strictRoleMapping: false, + }, + }, { name: "should return empty mapping when org mapping is nil", rawMapping: nil, @@ -277,8 +300,8 @@ func TestOrgRoleMapper_ParseOrgMappingSettings(t *testing.T) { }, }, { - name: "should return empty mapping when the org mapping format is invalid and strict role mapping is enabled", - rawMapping: []string{"External:Org1:First:Organization:Editor"}, + name: "should return empty mapping when one of the org mappings are not in the correct format and strict role mapping is enabled", + rawMapping: []string{"Second:Group:1:SuperEditor", "Second:1:Viewer"}, roleStrict: true, expected: &MappingConfiguration{ orgMapping: map[string]map[int64]org.RoleType{}, @@ -286,11 +309,11 @@ func TestOrgRoleMapper_ParseOrgMappingSettings(t *testing.T) { }, }, { - name: "should return only the valid mappings from the raw mappings when strict role mapping is disabled", - rawMapping: []string{"External:Org1:First:Organization:Editor", "Second:1:Editor"}, + name: "should skip org mapping when one of the org mappings are not in the correct format and strict role mapping is enabled", + rawMapping: []string{"Second:Group:1:SuperEditor", "Second:1:Admin"}, roleStrict: false, expected: &MappingConfiguration{ - orgMapping: map[string]map[int64]org.RoleType{"Second": {1: org.RoleEditor}}, + orgMapping: map[string]map[int64]org.RoleType{"Second": {1: org.RoleAdmin}}, strictRoleMapping: false, }, },