Commit b20a258b by gotjosh Committed by GitHub

LDAP: Show non-matched groups returned from LDAP (#19208)

* LDAP: Show all LDAP groups

* Use the returned LDAP groups as the reference when debugging LDAP

We need to use the LDAP groups returned as the main reference for
assuming what we were able to match and what wasn't. Before, we were
using the configured groups in LDAP TOML configuration file.

* s/User name/Username

* Add a title to for the LDAP mapping results

* LDAP: UI Updates to debug view

* LDAP: Make it explicit when we weren't able to match teams
parent 98c95a8a
...@@ -34,7 +34,7 @@ type LDAPAttribute struct { ...@@ -34,7 +34,7 @@ type LDAPAttribute struct {
} }
// RoleDTO is a serializer for mapped roles from LDAP // RoleDTO is a serializer for mapped roles from LDAP
type RoleDTO struct { type LDAPRoleDTO struct {
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"` OrgName string `json:"orgName"`
OrgRole models.RoleType `json:"orgRole"` OrgRole models.RoleType `json:"orgRole"`
...@@ -49,7 +49,7 @@ type LDAPUserDTO struct { ...@@ -49,7 +49,7 @@ type LDAPUserDTO struct {
Username *LDAPAttribute `json:"login"` Username *LDAPAttribute `json:"login"`
IsGrafanaAdmin *bool `json:"isGrafanaAdmin"` IsGrafanaAdmin *bool `json:"isGrafanaAdmin"`
IsDisabled bool `json:"isDisabled"` IsDisabled bool `json:"isDisabled"`
OrgRoles []RoleDTO `json:"roles"` OrgRoles []LDAPRoleDTO `json:"roles"`
Teams []models.TeamOrgGroupDTO `json:"teams"` Teams []models.TeamOrgGroupDTO `json:"teams"`
} }
...@@ -90,6 +90,10 @@ func (user *LDAPUserDTO) FetchOrgs() error { ...@@ -90,6 +90,10 @@ func (user *LDAPUserDTO) FetchOrgs() error {
} }
for i, orgDTO := range user.OrgRoles { for i, orgDTO := range user.OrgRoles {
if orgDTO.OrgId < 1 {
continue
}
orgName := orgNamesById[orgDTO.OrgId] orgName := orgNamesById[orgDTO.OrgId]
if orgName != "" { if orgName != "" {
...@@ -256,7 +260,7 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response { ...@@ -256,7 +260,7 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
user, serverConfig, err := ldap.User(username) user, serverConfig, err := ldap.User(username)
if user == nil { if user == nil {
return Error(http.StatusNotFound, "No user was found on the LDAP server(s)", err) return Error(http.StatusNotFound, "No user was found in the LDAP server(s) with that username", err)
} }
logger.Debug("user found", "user", user) logger.Debug("user found", "user", user)
...@@ -272,22 +276,32 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response { ...@@ -272,22 +276,32 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
IsDisabled: user.IsDisabled, IsDisabled: user.IsDisabled,
} }
orgRoles := []RoleDTO{} orgRoles := []LDAPRoleDTO{}
for _, g := range serverConfig.Groups { // First, let's find the groupDN that we did match by inspecting the assigned user OrgRoles.
role := &RoleDTO{} for _, group := range serverConfig.Groups {
orgRole, ok := user.OrgRoles[group.OrgId]
if isMatchToLDAPGroup(user, g) { if ok && orgRole == group.OrgRole {
role.OrgId = g.OrgID r := &LDAPRoleDTO{GroupDN: group.GroupDN, OrgId: group.OrgId, OrgRole: group.OrgRole}
role.OrgRole = user.OrgRoles[g.OrgID] orgRoles = append(orgRoles, *r)
role.GroupDN = g.GroupDN }
}
orgRoles = append(orgRoles, *role) // Then, we find what we did not match by inspecting the list of groups returned from
} else { // LDAP against what we have already matched above.
role.OrgId = g.OrgID for _, userGroup := range user.Groups {
role.GroupDN = g.GroupDN var matches int
orgRoles = append(orgRoles, *role) for _, orgRole := range orgRoles {
if orgRole.GroupDN == userGroup { // we already matched it
matches++
}
}
if matches < 1 {
r := &LDAPRoleDTO{GroupDN: userGroup}
orgRoles = append(orgRoles, *r)
} }
} }
...@@ -312,12 +326,6 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response { ...@@ -312,12 +326,6 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
return JSON(200, u) return JSON(200, u)
} }
// isMatchToLDAPGroup determines if we were able to match an LDAP group to an organization+role.
// Since we allow one role per organization. If it's set, we were able to match it.
func isMatchToLDAPGroup(user *models.ExternalUserInfo, groupConfig *ldap.GroupToOrgRole) bool {
return user.OrgRoles[groupConfig.OrgID] == groupConfig.OrgRole
}
// splitName receives the full name of a user and splits it into two parts: A name and a surname. // splitName receives the full name of a user and splits it into two parts: A name and a surname.
func splitName(name string) (string, string) { func splitName(name string) (string, string) {
names := util.SplitString(name) names := util.SplitString(name)
......
...@@ -94,7 +94,7 @@ func TestGetUserFromLDAPApiEndpoint_UserNotFound(t *testing.T) { ...@@ -94,7 +94,7 @@ func TestGetUserFromLDAPApiEndpoint_UserNotFound(t *testing.T) {
sc := getUserFromLDAPContext(t, "/api/admin/ldap/user-that-does-not-exist") sc := getUserFromLDAPContext(t, "/api/admin/ldap/user-that-does-not-exist")
require.Equal(t, sc.resp.Code, http.StatusNotFound) require.Equal(t, sc.resp.Code, http.StatusNotFound)
assert.JSONEq(t, "{\"message\":\"No user was found on the LDAP server(s)\"}", sc.resp.Body.String()) assert.JSONEq(t, "{\"message\":\"No user was found in the LDAP server(s) with that username\"}", sc.resp.Body.String())
} }
func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
...@@ -103,6 +103,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { ...@@ -103,6 +103,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
Name: "John Doe", Name: "John Doe",
Email: "john.doe@example.com", Email: "john.doe@example.com",
Login: "johndoe", Login: "johndoe",
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN, 2: models.ROLE_VIEWER}, OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN, 2: models.ROLE_VIEWER},
IsGrafanaAdmin: &isAdmin, IsGrafanaAdmin: &isAdmin,
} }
...@@ -117,12 +118,12 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { ...@@ -117,12 +118,12 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
Groups: []*ldap.GroupToOrgRole{ Groups: []*ldap.GroupToOrgRole{
{ {
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org", GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
OrgID: 1, OrgId: 1,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
}, },
{ {
GroupDN: "cn=admins,ou=groups,dc=grafana2,dc=org", GroupDN: "cn=admins,ou=groups,dc=grafana2,dc=org",
OrgID: 2, OrgId: 2,
OrgRole: models.ROLE_VIEWER, OrgRole: models.ROLE_VIEWER,
}, },
}, },
...@@ -164,6 +165,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { ...@@ -164,6 +165,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
Name: "John Doe", Name: "John Doe",
Email: "john.doe@example.com", Email: "john.doe@example.com",
Login: "johndoe", Login: "johndoe",
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org", "another-group-not-matched"},
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN}, OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN},
IsGrafanaAdmin: &isAdmin, IsGrafanaAdmin: &isAdmin,
} }
...@@ -178,7 +180,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { ...@@ -178,7 +180,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
Groups: []*ldap.GroupToOrgRole{ Groups: []*ldap.GroupToOrgRole{
{ {
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org", GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
OrgID: 1, OrgId: 1,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
}, },
}, },
...@@ -203,7 +205,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { ...@@ -203,7 +205,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe") sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe")
require.Equal(t, sc.resp.Code, http.StatusOK) assert.Equal(t, sc.resp.Code, http.StatusOK)
expected := ` expected := `
{ {
...@@ -222,7 +224,8 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { ...@@ -222,7 +224,8 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
"isGrafanaAdmin": true, "isGrafanaAdmin": true,
"isDisabled": false, "isDisabled": false,
"roles": [ "roles": [
{ "orgId": 1, "orgRole": "Admin", "orgName": "Main Org.", "groupDN": "cn=admins,ou=groups,dc=grafana,dc=org" } { "orgId": 1, "orgRole": "Admin", "orgName": "Main Org.", "groupDN": "cn=admins,ou=groups,dc=grafana,dc=org" },
{ "orgId": 0, "orgRole": "", "orgName": "", "groupDN": "another-group-not-matched" }
], ],
"teams": null "teams": null
} }
...@@ -251,7 +254,7 @@ func TestGetUserFromLDAPApiEndpoint_WithTeamHandler(t *testing.T) { ...@@ -251,7 +254,7 @@ func TestGetUserFromLDAPApiEndpoint_WithTeamHandler(t *testing.T) {
Groups: []*ldap.GroupToOrgRole{ Groups: []*ldap.GroupToOrgRole{
{ {
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org", GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
OrgID: 1, OrgId: 1,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
}, },
}, },
......
...@@ -408,12 +408,12 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn ...@@ -408,12 +408,12 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn
for _, group := range server.Config.Groups { for _, group := range server.Config.Groups {
// only use the first match for each org // only use the first match for each org
if extUser.OrgRoles[group.OrgID] != "" { if extUser.OrgRoles[group.OrgId] != "" {
continue continue
} }
if isMemberOf(memberOf, group.GroupDN) { if isMemberOf(memberOf, group.GroupDN) {
extUser.OrgRoles[group.OrgID] = group.OrgRole extUser.OrgRoles[group.OrgId] = group.OrgRole
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin { if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
} }
......
...@@ -3,11 +3,10 @@ package ldap ...@@ -3,11 +3,10 @@ package ldap
import ( import (
"testing" "testing"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ldap.v3"
) )
func TestLDAPPrivateMethods(t *testing.T) { func TestLDAPPrivateMethods(t *testing.T) {
...@@ -124,7 +123,7 @@ func TestLDAPPrivateMethods(t *testing.T) { ...@@ -124,7 +123,7 @@ func TestLDAPPrivateMethods(t *testing.T) {
Config: &ServerConfig{ Config: &ServerConfig{
Groups: []*GroupToOrgRole{ Groups: []*GroupToOrgRole{
{ {
OrgID: 1, OrgId: 1,
}, },
}, },
}, },
...@@ -162,7 +161,7 @@ func TestLDAPPrivateMethods(t *testing.T) { ...@@ -162,7 +161,7 @@ func TestLDAPPrivateMethods(t *testing.T) {
Config: &ServerConfig{ Config: &ServerConfig{
Groups: []*GroupToOrgRole{ Groups: []*GroupToOrgRole{
{ {
OrgID: 1, OrgId: 1,
}, },
}, },
}, },
......
...@@ -55,7 +55,7 @@ type AttributeMap struct { ...@@ -55,7 +55,7 @@ type AttributeMap struct {
// config "group_mappings" setting // config "group_mappings" setting
type GroupToOrgRole struct { type GroupToOrgRole struct {
GroupDN string `toml:"group_dn"` GroupDN string `toml:"group_dn"`
OrgID int64 `toml:"org_id"` OrgId int64 `toml:"org_id"`
// This pointer specifies if setting was set (for backwards compatibility) // This pointer specifies if setting was set (for backwards compatibility)
IsGrafanaAdmin *bool `toml:"grafana_admin"` IsGrafanaAdmin *bool `toml:"grafana_admin"`
...@@ -139,8 +139,8 @@ func readConfig(configFile string) (*Config, error) { ...@@ -139,8 +139,8 @@ func readConfig(configFile string) (*Config, error) {
} }
for _, groupMap := range server.Groups { for _, groupMap := range server.Groups {
if groupMap.OrgID == 0 { if groupMap.OrgId == 0 {
groupMap.OrgID = 1 groupMap.OrgId = 1
} }
} }
} }
......
...@@ -89,12 +89,12 @@ export class LdapPage extends PureComponent<Props, State> { ...@@ -89,12 +89,12 @@ export class LdapPage extends PureComponent<Props, State> {
{config.buildInfo.isEnterprise && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />} {config.buildInfo.isEnterprise && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />}
<h3 className="page-heading">User mapping</h3> <h3 className="page-heading">Test user mapping</h3>
<div className="gf-form-group"> <div className="gf-form-group">
<form onSubmit={this.search} className="gf-form-inline"> <form onSubmit={this.search} className="gf-form-inline">
<FormField label="User name" labelWidth={8} inputWidth={30} type="text" id="username" name="username" /> <FormField label="Username" labelWidth={8} inputWidth={30} type="text" id="username" name="username" />
<button type="submit" className="btn btn-primary"> <button type="submit" className="btn btn-primary">
Test LDAP mapping Run
</button> </button>
</form> </form>
</div> </div>
......
...@@ -9,7 +9,6 @@ interface Props { ...@@ -9,7 +9,6 @@ interface Props {
export const LdapUserGroups: FC<Props> = ({ groups, showAttributeMapping }) => { export const LdapUserGroups: FC<Props> = ({ groups, showAttributeMapping }) => {
const items = showAttributeMapping ? groups : groups.filter(item => item.orgRole); const items = showAttributeMapping ? groups : groups.filter(item => item.orgRole);
const roleColumnClass = showAttributeMapping && 'width-14';
return ( return (
<div className="gf-form-group"> <div className="gf-form-group">
...@@ -17,32 +16,39 @@ export const LdapUserGroups: FC<Props> = ({ groups, showAttributeMapping }) => { ...@@ -17,32 +16,39 @@ export const LdapUserGroups: FC<Props> = ({ groups, showAttributeMapping }) => {
<table className="filter-table form-inline"> <table className="filter-table form-inline">
<thead> <thead>
<tr> <tr>
{showAttributeMapping && <th>LDAP Group</th>}
<th>Organisation</th> <th>Organisation</th>
<th>Role</th> <th>Role</th>
{showAttributeMapping && <th colSpan={2}>LDAP Group</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{items.map((group, index) => { {items.map((group, index) => {
return ( return (
<tr key={`${group.orgId}-${index}`}> <tr key={`${group.orgId}-${index}`}>
<td className="width-16">{group.orgName}</td>
<td className={roleColumnClass}>{group.orgRole}</td>
{showAttributeMapping && ( {showAttributeMapping && (
<> <>
<td>{group.groupDN}</td> <td>{group.groupDN}</td>
<td> {!group.orgRole && (
{!group.orgRole && ( <>
<span className="text-warning pull-right"> <td />
No match <td>
<Tooltip placement="top" content="No matching groups found" theme={'info'}> <span className="text-warning">
<div className="gf-form-help-icon gf-form-help-icon--right-normal"> No match
<i className="fa fa-info-circle" /> <Tooltip placement="top" content="No matching groups found" theme={'info'}>
</div> <span className="gf-form-help-icon">
</Tooltip> <i className="fa fa-info-circle" />
</span> </span>
)} </Tooltip>
</td> </span>
</td>
</>
)}
</>
)}
{group.orgName && (
<>
<td>{group.orgName}</td>
<td>{group.orgRole}</td>
</> </>
)} )}
</tr> </tr>
......
...@@ -18,8 +18,21 @@ export const LdapUserInfo: FC<Props> = ({ ldapUser, showAttributeMapping }) => { ...@@ -18,8 +18,21 @@ export const LdapUserInfo: FC<Props> = ({ ldapUser, showAttributeMapping }) => {
{ldapUser.roles && ldapUser.roles.length > 0 && ( {ldapUser.roles && ldapUser.roles.length > 0 && (
<LdapUserGroups groups={ldapUser.roles} showAttributeMapping={showAttributeMapping} /> <LdapUserGroups groups={ldapUser.roles} showAttributeMapping={showAttributeMapping} />
)} )}
{ldapUser.teams && ldapUser.teams.length > 0 && (
{ldapUser.teams && ldapUser.teams.length > 0 ? (
<LdapUserTeams teams={ldapUser.teams} showAttributeMapping={showAttributeMapping} /> <LdapUserTeams teams={ldapUser.teams} showAttributeMapping={showAttributeMapping} />
) : (
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<tbody>
<tr>
<td>No teams found via LDAP</td>
</tr>
</tbody>
</table>
</div>
</div>
)} )}
</> </>
); );
......
import React, { FC } from 'react'; import React, { FC } from 'react';
import { css } from 'emotion';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import { LdapTeam } from 'app/types'; import { LdapTeam } from 'app/types';
...@@ -10,10 +9,6 @@ interface Props { ...@@ -10,10 +9,6 @@ interface Props {
export const LdapUserTeams: FC<Props> = ({ teams, showAttributeMapping }) => { export const LdapUserTeams: FC<Props> = ({ teams, showAttributeMapping }) => {
const items = showAttributeMapping ? teams : teams.filter(item => item.teamName); const items = showAttributeMapping ? teams : teams.filter(item => item.teamName);
const teamColumnClass = showAttributeMapping && 'width-14';
const noMatchPlaceholderStyle = css`
display: flex;
`;
return ( return (
<div className="gf-form-group"> <div className="gf-form-group">
...@@ -21,29 +16,41 @@ export const LdapUserTeams: FC<Props> = ({ teams, showAttributeMapping }) => { ...@@ -21,29 +16,41 @@ export const LdapUserTeams: FC<Props> = ({ teams, showAttributeMapping }) => {
<table className="filter-table form-inline"> <table className="filter-table form-inline">
<thead> <thead>
<tr> <tr>
{showAttributeMapping && <th>LDAP Group</th>}
<th>Organisation</th> <th>Organisation</th>
<th>Team</th> <th>Team</th>
{showAttributeMapping && <th>LDAP</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{items.map((team, index) => { {items.map((team, index) => {
return ( return (
<tr key={`${team.teamName}-${index}`}> <tr key={`${team.teamName}-${index}`}>
<td className="width-16"> {showAttributeMapping && (
{team.orgName || ( <>
<div className={`text-warning ${noMatchPlaceholderStyle}`}> <td>{team.groupDN}</td>
No match {!team.orgName && (
<Tooltip placement="top" content="No matching teams found" theme={'info'}> <>
<div className="gf-form-help-icon gf-form-help-icon--right-normal"> <td />
<i className="fa fa-info-circle" /> <td>
</div> <div className="text-warning">
</Tooltip> No match
</div> <Tooltip placement="top" content="No matching teams found" theme={'info'}>
)} <span className="gf-form-help-icon">
</td> <i className="fa fa-info-circle" />
<td className={teamColumnClass}>{team.teamName}</td> </span>
{showAttributeMapping && <td>{team.groupDN}</td>} </Tooltip>
</div>
</td>
</>
)}
</>
)}
{team.orgName && (
<>
<td>{team.orgName}</td>
<td>{team.teamName}</td>
</>
)}
</tr> </tr>
); );
})} })}
......
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
.page-heading { .page-heading {
font-size: $font-size-h4; font-size: $font-size-h4;
margin-top: 0; margin-top: 0;
margin-bottom: $spacer * 0.7; margin-bottom: $spacer;
} }
.page-action-bar { .page-action-bar {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment