2019-09-03 12:34:44 -05:00
package api
import (
2019-09-13 10:26:25 -05:00
"context"
2019-09-03 12:34:44 -05:00
"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-13 10:26:25 -05:00
tokenService = AuthToken { } . TokenService
2019-09-03 12:34:44 -05:00
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 {
return fmt . Errorf ( "Unable to find organization with ID '%d'" , orgId )
}
)
// 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
type RoleDTO struct {
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" `
OrgRoles [ ] RoleDTO ` json:"roles" `
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" `
}
type AuthToken struct {
TokenService TokenRevoker ` inject:"" `
}
type TokenRevoker interface {
RevokeAllUserTokens ( context . Context , int64 ) 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 {
orgName := orgNamesById [ orgDTO . OrgId ]
if orgName != "" {
user . OrgRoles [ i ] . OrgName = orgName
} else {
return errOrganizationNotFound ( orgDTO . OrgId )
}
}
return nil
}
// ReloadLDAPCfg reloads the LDAP configuration
func ( server * HTTPServer ) ReloadLDAPCfg ( ) Response {
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" )
}
2019-09-04 09:29:14 -05:00
// GetLDAPStatus attempts to connect to all the configured LDAP servers and returns information on whenever they're availabe or not.
func ( server * HTTPServer ) GetLDAPStatus ( c * models . ReqContext ) Response {
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
func ( server * HTTPServer ) PostSyncUserWithLDAP ( c * models . ReqContext ) Response {
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 {
if err == ldap . ErrCouldNotFindUser { // User was not in the LDAP server - we need to take action:
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 )
}
err = tokenService . RevokeAllUserTokens ( context . TODO ( ) , userId )
if err != nil {
return Error ( http . StatusInternalServerError , "Failed to remove session tokens for the user" , err )
}
return Success ( "User disabled without any updates in the information" ) // should this be a success?
}
}
upsertCmd := & models . UpsertUserCommand {
ExternalUser : user ,
SignupAllowed : setting . LDAPAllowSignup ,
}
err = bus . Dispatch ( upsertCmd )
if err != nil {
return Error ( http . StatusInternalServerError , "Failed to udpate the user" , err )
}
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.
func ( server * 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 {
return Error ( http . StatusNotFound , "No user was found on the LDAP server(s)" , err )
}
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 ,
}
orgRoles := [ ] RoleDTO { }
for _ , g := range serverConfig . Groups {
role := & RoleDTO { }
if isMatchToLDAPGroup ( user , g ) {
role . OrgId = g . OrgID
role . OrgRole = user . OrgRoles [ g . OrgID ]
role . GroupDN = g . GroupDN
orgRoles = append ( orgRoles , * role )
} else {
role . OrgId = g . OrgID
role . GroupDN = g . GroupDN
orgRoles = append ( orgRoles , * role )
}
}
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 {
2019-09-08 05:48:47 -05:00
return Error ( http . StatusBadRequest , "An oganization 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 )
}
// 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.
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 ]
}
}