2019-09-03 12:34:44 -05:00
package api
import (
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/bus"
2019-09-08 05:48:47 -05:00
"github.com/grafana/grafana/pkg/infra/log"
2019-09-13 10:26:25 -05:00
"github.com/grafana/grafana/pkg/login"
2019-09-03 12:34:44 -05:00
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap"
2019-09-13 10:26:25 -05:00
"github.com/grafana/grafana/pkg/setting"
2019-09-03 12:34:44 -05:00
"github.com/grafana/grafana/pkg/util"
)
var (
getLDAPConfig = multildap . GetConfig
newLDAP = multildap . New
2019-09-08 05:48:47 -05:00
logger = log . New ( "LDAP.debug" )
2019-09-03 12:34:44 -05:00
errOrganizationNotFound = func ( orgId int64 ) error {
2020-11-05 06:07:06 -06:00
return fmt . Errorf ( "unable to find organization with ID '%d'" , orgId )
2019-09-03 12:34:44 -05:00
}
)
// LDAPAttribute is a serializer for user attributes mapped from LDAP. Is meant to display both the serialized value and the LDAP key we received it from.
type LDAPAttribute struct {
ConfigAttributeValue string ` json:"cfgAttrValue" `
LDAPAttributeValue string ` json:"ldapValue" `
}
// RoleDTO is a serializer for mapped roles from LDAP
2019-09-19 10:13:38 -05:00
type LDAPRoleDTO struct {
2019-09-03 12:34:44 -05:00
OrgId int64 ` json:"orgId" `
OrgName string ` json:"orgName" `
OrgRole models . RoleType ` json:"orgRole" `
GroupDN string ` json:"groupDN" `
}
// LDAPUserDTO is a serializer for users mapped from LDAP
type LDAPUserDTO struct {
2019-09-08 05:48:47 -05:00
Name * LDAPAttribute ` json:"name" `
Surname * LDAPAttribute ` json:"surname" `
Email * LDAPAttribute ` json:"email" `
Username * LDAPAttribute ` json:"login" `
IsGrafanaAdmin * bool ` json:"isGrafanaAdmin" `
IsDisabled bool ` json:"isDisabled" `
2019-09-19 10:13:38 -05:00
OrgRoles [ ] LDAPRoleDTO ` json:"roles" `
2019-09-08 05:48:47 -05:00
Teams [ ] models . TeamOrgGroupDTO ` json:"teams" `
2019-09-03 12:34:44 -05:00
}
2019-09-13 10:26:25 -05:00
// LDAPServerDTO is a serializer for LDAP server statuses
type LDAPServerDTO struct {
Host string ` json:"host" `
Port int ` json:"port" `
Available bool ` json:"available" `
Error string ` json:"error" `
}
2019-09-03 12:34:44 -05:00
// FetchOrgs fetches the organization(s) information by executing a single query to the database. Then, populating the DTO with the information retrieved.
func ( user * LDAPUserDTO ) FetchOrgs ( ) error {
orgIds := [ ] int64 { }
for _ , or := range user . OrgRoles {
orgIds = append ( orgIds , or . OrgId )
}
q := & models . SearchOrgsQuery { }
q . Ids = orgIds
if err := bus . Dispatch ( q ) ; err != nil {
return err
}
orgNamesById := map [ int64 ] string { }
for _ , org := range q . Result {
orgNamesById [ org . Id ] = org . Name
}
for i , orgDTO := range user . OrgRoles {
2019-09-19 10:13:38 -05:00
if orgDTO . OrgId < 1 {
continue
}
2019-09-03 12:34:44 -05:00
orgName := orgNamesById [ orgDTO . OrgId ]
if orgName != "" {
user . OrgRoles [ i ] . OrgName = orgName
} else {
return errOrganizationNotFound ( orgDTO . OrgId )
}
}
return nil
}
// ReloadLDAPCfg reloads the LDAP configuration
2020-11-05 08:37:11 -06:00
func ( hs * HTTPServer ) ReloadLDAPCfg ( ) Response {
2019-09-03 12:34:44 -05:00
if ! ldap . IsEnabled ( ) {
2019-09-04 09:29:14 -05:00
return Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
2019-09-03 12:34:44 -05:00
}
err := ldap . ReloadConfig ( )
if err != nil {
2019-09-13 10:26:25 -05:00
return Error ( http . StatusInternalServerError , "Failed to reload LDAP config" , err )
2019-09-03 12:34:44 -05:00
}
return Success ( "LDAP config reloaded" )
}
2020-04-29 14:37:21 -05:00
// GetLDAPStatus attempts to connect to all the configured LDAP servers and returns information on whenever they're available or not.
2020-11-05 08:37:11 -06:00
func ( hs * HTTPServer ) GetLDAPStatus ( c * models . ReqContext ) Response {
2019-09-04 09:29:14 -05:00
if ! ldap . IsEnabled ( ) {
return Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
}
ldapConfig , err := getLDAPConfig ( )
if err != nil {
2019-09-13 10:26:25 -05:00
return Error ( http . StatusBadRequest , "Failed to obtain the LDAP configuration. Please verify the configuration and try again" , err )
2019-09-04 09:29:14 -05:00
}
ldap := newLDAP ( ldapConfig . Servers )
2019-09-16 10:56:01 -05:00
if ldap == nil {
return Error ( http . StatusInternalServerError , "Failed to find the LDAP server" , nil )
}
2019-09-04 09:29:14 -05:00
statuses , err := ldap . Ping ( )
if err != nil {
return Error ( http . StatusBadRequest , "Failed to connect to the LDAP server(s)" , err )
}
serverDTOs := [ ] * LDAPServerDTO { }
for _ , status := range statuses {
s := & LDAPServerDTO {
Host : status . Host ,
Available : status . Available ,
Port : status . Port ,
}
if status . Error != nil {
s . Error = status . Error . Error ( )
}
serverDTOs = append ( serverDTOs , s )
}
return JSON ( http . StatusOK , serverDTOs )
}
2019-09-13 10:26:25 -05:00
// PostSyncUserWithLDAP enables a single Grafana user to be synchronized against LDAP
2020-11-05 08:37:11 -06:00
func ( hs * HTTPServer ) PostSyncUserWithLDAP ( c * models . ReqContext ) Response {
2019-09-13 10:26:25 -05:00
if ! ldap . IsEnabled ( ) {
return Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
}
ldapConfig , err := getLDAPConfig ( )
if err != nil {
return Error ( http . StatusBadRequest , "Failed to obtain the LDAP configuration. Please verify the configuration and try again" , err )
}
userId := c . ParamsInt64 ( ":id" )
query := models . GetUserByIdQuery { Id : userId }
if err := bus . Dispatch ( & query ) ; err != nil { // validate the userId exists
if err == models . ErrUserNotFound {
return Error ( 404 , models . ErrUserNotFound . Error ( ) , nil )
}
return Error ( 500 , "Failed to get user" , err )
}
authModuleQuery := & models . GetAuthInfoQuery { UserId : query . Result . Id , AuthModule : models . AuthModuleLDAP }
if err := bus . Dispatch ( authModuleQuery ) ; err != nil { // validate the userId comes from LDAP
if err == models . ErrUserNotFound {
return Error ( 404 , models . ErrUserNotFound . Error ( ) , nil )
}
return Error ( 500 , "Failed to get user" , err )
}
ldapServer := newLDAP ( ldapConfig . Servers )
user , _ , err := ldapServer . User ( query . Result . Login )
if err != nil {
2019-11-07 07:31:44 -06:00
if err == multildap . ErrDidNotFindUser { // User was not in the LDAP server - we need to take action:
2019-09-13 10:26:25 -05:00
if setting . AdminUser == query . Result . Login { // User is *the* Grafana Admin. We cannot disable it.
errMsg := fmt . Sprintf ( ` Refusing to sync grafana super admin "%s" - it would be disabled ` , query . Result . Login )
logger . Error ( errMsg )
return Error ( http . StatusBadRequest , errMsg , err )
}
// Since the user was not in the LDAP server. Let's disable it.
err := login . DisableExternalUser ( query . Result . Login )
if err != nil {
return Error ( http . StatusInternalServerError , "Failed to disable the user" , err )
}
2020-11-05 08:37:11 -06:00
err = hs . AuthTokenService . RevokeAllUserTokens ( c . Req . Context ( ) , userId )
2019-09-13 10:26:25 -05:00
if err != nil {
return Error ( http . StatusInternalServerError , "Failed to remove session tokens for the user" , err )
}
2019-11-07 07:31:44 -06:00
return Error ( http . StatusBadRequest , "User not found in LDAP. Disabled the user without updating information" , nil ) // should this be a success?
2019-09-13 10:26:25 -05:00
}
2019-11-07 07:31:44 -06:00
logger . Debug ( "Failed to sync the user with LDAP" , "err" , err )
return Error ( http . StatusBadRequest , "Something went wrong while finding the user in LDAP" , err )
2019-09-13 10:26:25 -05:00
}
upsertCmd := & models . UpsertUserCommand {
2020-02-06 00:49:58 -06:00
ReqContext : c ,
2019-09-13 10:26:25 -05:00
ExternalUser : user ,
SignupAllowed : setting . LDAPAllowSignup ,
}
err = bus . Dispatch ( upsertCmd )
if err != nil {
2020-04-29 14:37:21 -05:00
return Error ( http . StatusInternalServerError , "Failed to update the user" , err )
2019-09-13 10:26:25 -05:00
}
return Success ( "User synced successfully" )
}
2019-09-03 12:34:44 -05:00
// GetUserFromLDAP finds an user based on a username in LDAP. This helps illustrate how would the particular user be mapped in Grafana when synced.
2020-11-05 08:37:11 -06:00
func ( hs * HTTPServer ) GetUserFromLDAP ( c * models . ReqContext ) Response {
2019-09-04 09:29:14 -05:00
if ! ldap . IsEnabled ( ) {
return Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
}
2019-09-03 12:34:44 -05:00
ldapConfig , err := getLDAPConfig ( )
if err != nil {
2019-09-08 05:48:47 -05:00
return Error ( http . StatusBadRequest , "Failed to obtain the LDAP configuration" , err )
2019-09-03 12:34:44 -05:00
}
ldap := newLDAP ( ldapConfig . Servers )
username := c . Params ( ":username" )
if len ( username ) == 0 {
return Error ( http . StatusBadRequest , "Validation error. You must specify an username" , nil )
}
user , serverConfig , err := ldap . User ( username )
if user == nil {
2019-09-19 10:13:38 -05:00
return Error ( http . StatusNotFound , "No user was found in the LDAP server(s) with that username" , err )
2019-09-03 12:34:44 -05:00
}
2019-09-08 05:48:47 -05:00
logger . Debug ( "user found" , "user" , user )
2019-09-03 12:34:44 -05:00
name , surname := splitName ( user . Name )
u := & LDAPUserDTO {
Name : & LDAPAttribute { serverConfig . Attr . Name , name } ,
Surname : & LDAPAttribute { serverConfig . Attr . Surname , surname } ,
Email : & LDAPAttribute { serverConfig . Attr . Email , user . Email } ,
Username : & LDAPAttribute { serverConfig . Attr . Username , user . Login } ,
IsGrafanaAdmin : user . IsGrafanaAdmin ,
IsDisabled : user . IsDisabled ,
}
2019-09-19 10:13:38 -05:00
orgRoles := [ ] LDAPRoleDTO { }
2019-09-03 12:34:44 -05:00
2019-11-01 09:42:22 -05:00
// Need to iterate based on the config groups as only the first match for an org is used
// We are showing all matches as that should help in understanding why one match wins out
// over another.
for _ , configGroup := range serverConfig . Groups {
for _ , userGroup := range user . Groups {
if configGroup . GroupDN == userGroup {
r := & LDAPRoleDTO { GroupDN : configGroup . GroupDN , OrgId : configGroup . OrgId , OrgRole : configGroup . OrgRole }
orgRoles = append ( orgRoles , * r )
break
}
2019-09-19 10:13:38 -05:00
}
2019-11-01 09:42:22 -05:00
//}
2019-09-19 10:13:38 -05:00
}
2019-09-03 12:34:44 -05:00
2019-09-19 10:13:38 -05:00
// Then, we find what we did not match by inspecting the list of groups returned from
// LDAP against what we have already matched above.
for _ , userGroup := range user . Groups {
2019-11-01 09:42:22 -05:00
var matched bool
2019-09-03 12:34:44 -05:00
2019-09-19 10:13:38 -05:00
for _ , orgRole := range orgRoles {
if orgRole . GroupDN == userGroup { // we already matched it
2019-11-01 09:42:22 -05:00
matched = true
break
2019-09-19 10:13:38 -05:00
}
}
2019-11-01 09:42:22 -05:00
if ! matched {
2019-09-19 10:13:38 -05:00
r := & LDAPRoleDTO { GroupDN : userGroup }
orgRoles = append ( orgRoles , * r )
2019-09-03 12:34:44 -05:00
}
}
u . OrgRoles = orgRoles
2019-09-08 05:48:47 -05:00
logger . Debug ( "mapping org roles" , "orgsRoles" , u . OrgRoles )
2019-09-03 12:34:44 -05:00
err = u . FetchOrgs ( )
if err != nil {
2020-06-01 10:11:25 -05:00
return Error ( http . StatusBadRequest , "An organization was not found - Please verify your LDAP configuration" , err )
2019-09-03 12:34:44 -05:00
}
2019-09-08 05:48:47 -05:00
cmd := & models . GetTeamsForLDAPGroupCommand { Groups : user . Groups }
err = bus . Dispatch ( cmd )
if err != bus . ErrHandlerNotFound && err != nil {
return Error ( http . StatusBadRequest , "Unable to find the teams for this user" , err )
}
u . Teams = cmd . Result
2019-09-03 12:34:44 -05:00
return JSON ( 200 , u )
}
// 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 ) {
names := util . SplitString ( name )
switch len ( names ) {
case 0 :
return "" , ""
case 1 :
return names [ 0 ] , ""
default :
return names [ 0 ] , names [ 1 ]
}
}