2019-09-03 12:34:44 -05:00
package api
import (
2021-11-08 08:53:51 -06:00
"context"
2020-11-19 06:34:28 -06:00
"errors"
2019-09-03 12:34:44 -05:00
"fmt"
"net/http"
2022-01-14 10:55:57 -06:00
"strconv"
2022-04-21 09:38:55 -05:00
"strings"
2019-09-03 12:34:44 -05:00
2021-01-15 07:43:20 -06:00
"github.com/grafana/grafana/pkg/api/response"
2019-09-08 05:48:47 -05:00
"github.com/grafana/grafana/pkg/infra/log"
2019-09-03 12:34:44 -05:00
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
2022-08-10 03:21:33 -05:00
"github.com/grafana/grafana/pkg/services/login"
2019-09-03 12:34:44 -05:00
"github.com/grafana/grafana/pkg/services/multildap"
2022-08-10 04:56:48 -05:00
"github.com/grafana/grafana/pkg/services/org"
2022-02-09 04:45:31 -06:00
"github.com/grafana/grafana/pkg/services/sqlstore"
2022-07-20 07:50:06 -05:00
"github.com/grafana/grafana/pkg/services/user"
2019-09-03 12:34:44 -05:00
"github.com/grafana/grafana/pkg/util"
2021-10-11 07:30:59 -05:00
"github.com/grafana/grafana/pkg/web"
2019-09-03 12:34:44 -05:00
)
var (
getLDAPConfig = multildap . GetConfig
newLDAP = multildap . New
2020-12-15 02:32:06 -06:00
ldapLogger = log . New ( "LDAP.debug" )
2019-09-08 05:48:47 -05:00
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 {
2022-08-10 04:56:48 -05:00
OrgId int64 ` json:"orgId" `
OrgName string ` json:"orgName" `
OrgRole org . RoleType ` json:"orgRole" `
GroupDN string ` json:"groupDN" `
2019-09-03 12:34:44 -05:00
}
// 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.
2022-02-09 04:45:31 -06:00
func ( user * LDAPUserDTO ) FetchOrgs ( ctx context . Context , sqlstore sqlstore . Store ) error {
2019-09-03 12:34:44 -05:00
orgIds := [ ] int64 { }
for _ , or := range user . OrgRoles {
orgIds = append ( orgIds , or . OrgId )
}
q := & models . SearchOrgsQuery { }
q . Ids = orgIds
2022-02-09 04:45:31 -06:00
if err := sqlstore . SearchOrgs ( ctx , q ) ; err != nil {
2019-09-03 12:34:44 -05:00
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
}
2022-07-27 08:54:37 -05:00
// swagger:route POST /admin/ldap/reload admin_ldap reloadLDAPCfg
//
// Reloads the LDAP configuration.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.config:reload`.
//
// Security:
// - basic:
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
2021-11-29 03:18:01 -06:00
func ( hs * HTTPServer ) ReloadLDAPCfg ( c * models . ReqContext ) response . Response {
2019-09-03 12:34:44 -05:00
if ! ldap . IsEnabled ( ) {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
2019-09-03 12:34:44 -05:00
}
err := ldap . ReloadConfig ( )
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusInternalServerError , "Failed to reload LDAP config" , err )
2019-09-03 12:34:44 -05:00
}
2021-01-15 07:43:20 -06:00
return response . Success ( "LDAP config reloaded" )
2019-09-03 12:34:44 -05:00
}
2022-07-27 08:54:37 -05:00
// swagger:route GET /admin/ldap/status admin_ldap getLDAPStatus
//
// Attempts to connect to all the configured LDAP servers and returns information on whenever they're available or not.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.status:read`.
//
// Security:
// - basic:
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
2021-01-15 07:43:20 -06:00
func ( hs * HTTPServer ) GetLDAPStatus ( c * models . ReqContext ) response . Response {
2019-09-04 09:29:14 -05:00
if ! ldap . IsEnabled ( ) {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
2019-09-04 09:29:14 -05:00
}
2020-12-11 04:44:44 -06:00
ldapConfig , err := getLDAPConfig ( hs . Cfg )
2019-09-04 09:29:14 -05:00
if err != nil {
2021-01-15 07:43:20 -06:00
return response . 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 {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusInternalServerError , "Failed to find the LDAP server" , nil )
2019-09-16 10:56:01 -05:00
}
2019-09-04 09:29:14 -05:00
statuses , err := ldap . Ping ( )
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "Failed to connect to the LDAP server(s)" , err )
2019-09-04 09:29:14 -05:00
}
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 )
}
2021-01-15 07:43:20 -06:00
return response . JSON ( http . StatusOK , serverDTOs )
2019-09-04 09:29:14 -05:00
}
2022-07-27 08:54:37 -05:00
// swagger:route POST /admin/ldap/sync/{user_id} admin_ldap postSyncUserWithLDAP
//
// Enables a single Grafana user to be synchronized against LDAP.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.user:sync`.
//
// Security:
// - basic:
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
2021-01-15 07:43:20 -06:00
func ( hs * HTTPServer ) PostSyncUserWithLDAP ( c * models . ReqContext ) response . Response {
2019-09-13 10:26:25 -05:00
if ! ldap . IsEnabled ( ) {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
2019-09-13 10:26:25 -05:00
}
2020-12-11 04:44:44 -06:00
ldapConfig , err := getLDAPConfig ( hs . Cfg )
2019-09-13 10:26:25 -05:00
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "Failed to obtain the LDAP configuration. Please verify the configuration and try again" , err )
2019-09-13 10:26:25 -05:00
}
2022-01-14 10:55:57 -06:00
userId , err := strconv . ParseInt ( web . Params ( c . Req ) [ ":id" ] , 10 , 64 )
if err != nil {
return response . Error ( http . StatusBadRequest , "id is invalid" , err )
}
2019-09-13 10:26:25 -05:00
2022-08-02 09:58:05 -05:00
query := user . GetUserByIDQuery { ID : userId }
2019-09-13 10:26:25 -05:00
2022-08-02 09:58:05 -05:00
usr , err := hs . userService . GetByID ( c . Req . Context ( ) , & query )
if err != nil { // validate the userId exists
2022-07-20 07:50:06 -05:00
if errors . Is ( err , user . ErrUserNotFound ) {
return response . Error ( 404 , user . ErrUserNotFound . Error ( ) , nil )
2019-09-13 10:26:25 -05:00
}
2021-01-15 07:43:20 -06:00
return response . Error ( 500 , "Failed to get user" , err )
2019-09-13 10:26:25 -05:00
}
2022-08-10 03:21:33 -05:00
authModuleQuery := & models . GetAuthInfoQuery { UserId : usr . ID , AuthModule : login . LDAPAuthModule }
2022-02-09 04:45:31 -06:00
if err := hs . authInfoService . GetAuthInfo ( c . Req . Context ( ) , authModuleQuery ) ; err != nil { // validate the userId comes from LDAP
2022-07-20 07:50:06 -05:00
if errors . Is ( err , user . ErrUserNotFound ) {
return response . Error ( 404 , user . ErrUserNotFound . Error ( ) , nil )
2019-09-13 10:26:25 -05:00
}
2021-01-15 07:43:20 -06:00
return response . Error ( 500 , "Failed to get user" , err )
2019-09-13 10:26:25 -05:00
}
ldapServer := newLDAP ( ldapConfig . Servers )
2022-08-02 09:58:05 -05:00
userInfo , _ , err := ldapServer . User ( usr . Login )
2019-09-13 10:26:25 -05:00
if err != nil {
2020-11-19 06:34:28 -06:00
if errors . Is ( err , multildap . ErrDidNotFindUser ) { // User was not in the LDAP server - we need to take action:
2022-08-02 09:58:05 -05:00
if hs . Cfg . AdminUser == usr . Login { // User is *the* Grafana Admin. We cannot disable it.
errMsg := fmt . Sprintf ( ` Refusing to sync grafana super admin "%s" - it would be disabled ` , usr . Login )
2020-12-15 02:32:06 -06:00
ldapLogger . Error ( errMsg )
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , errMsg , err )
2019-09-13 10:26:25 -05:00
}
// Since the user was not in the LDAP server. Let's disable it.
2022-08-02 09:58:05 -05:00
err := hs . Login . DisableExternalUser ( c . Req . Context ( ) , usr . Login )
2019-09-13 10:26:25 -05:00
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusInternalServerError , "Failed to disable the user" , err )
2019-09-13 10:26:25 -05:00
}
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 {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusInternalServerError , "Failed to remove session tokens for the user" , err )
2019-09-13 10:26:25 -05:00
}
2021-01-15 07:43:20 -06:00
return response . 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
2020-12-15 02:32:06 -06:00
ldapLogger . Debug ( "Failed to sync the user with LDAP" , "err" , err )
2021-01-15 07:43:20 -06:00
return response . 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 ,
2022-08-02 09:58:05 -05:00
ExternalUser : userInfo ,
2020-12-11 04:44:44 -06:00
SignupAllowed : hs . Cfg . LDAPAllowSignup ,
2022-07-15 04:21:09 -05:00
UserLookupParams : models . UserLookupParams {
2022-08-02 09:58:05 -05:00
UserID : & usr . ID , // Upsert by ID only
2022-07-15 04:21:09 -05:00
Email : nil ,
Login : nil ,
} ,
2019-09-13 10:26:25 -05:00
}
2022-02-09 04:45:31 -06:00
err = hs . Login . UpsertUser ( c . Req . Context ( ) , upsertCmd )
2019-09-13 10:26:25 -05:00
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusInternalServerError , "Failed to update the user" , err )
2019-09-13 10:26:25 -05:00
}
2021-01-15 07:43:20 -06:00
return response . Success ( "User synced successfully" )
2019-09-13 10:26:25 -05:00
}
2022-07-27 08:54:37 -05:00
// swagger:route GET /admin/ldap/{user_name} admin_ldap getUserFromLDAP
//
// Finds an user based on a username in LDAP. This helps illustrate how would the particular user be mapped in Grafana when synced.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.user:read`.
//
// Security:
// - basic:
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
2021-01-15 07:43:20 -06:00
func ( hs * HTTPServer ) GetUserFromLDAP ( c * models . ReqContext ) response . Response {
2019-09-04 09:29:14 -05:00
if ! ldap . IsEnabled ( ) {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "LDAP is not enabled" , nil )
2019-09-04 09:29:14 -05:00
}
2020-12-11 04:44:44 -06:00
ldapConfig , err := getLDAPConfig ( hs . Cfg )
2019-09-03 12:34:44 -05:00
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "Failed to obtain the LDAP configuration" , err )
2019-09-03 12:34:44 -05:00
}
2022-04-22 08:45:54 -05:00
multiLDAP := newLDAP ( ldapConfig . Servers )
2019-09-03 12:34:44 -05:00
2021-10-11 07:30:59 -05:00
username := web . Params ( c . Req ) [ ":username" ]
2019-09-03 12:34:44 -05:00
if len ( username ) == 0 {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "Validation error. You must specify an username" , nil )
2019-09-03 12:34:44 -05:00
}
2022-04-22 08:45:54 -05:00
user , serverConfig , err := multiLDAP . User ( username )
if user == nil || err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusNotFound , "No user was found in the LDAP server(s) with that username" , err )
2019-09-03 12:34:44 -05:00
}
2020-12-15 02:32:06 -06:00
ldapLogger . Debug ( "user found" , "user" , user )
2019-09-08 05:48:47 -05:00
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 ,
}
2022-04-22 08:45:54 -05:00
unmappedUserGroups := map [ string ] struct { } { }
2019-09-19 10:13:38 -05:00
for _ , userGroup := range user . Groups {
2022-04-22 08:45:54 -05:00
unmappedUserGroups [ strings . ToLower ( userGroup ) ] = struct { } { }
}
2019-09-03 12:34:44 -05:00
2022-08-10 03:20:23 -05:00
orgIDs := [ ] int64 { } // IDs of the orgs the user is a member of
2022-08-10 04:56:48 -05:00
orgRolesMap := map [ int64 ] org . RoleType { }
2022-04-22 08:45:54 -05:00
for _ , group := range serverConfig . Groups {
// only use the first match for each org
if orgRolesMap [ group . OrgId ] != "" {
continue
2019-09-19 10:13:38 -05:00
}
2022-04-22 08:45:54 -05:00
if ldap . IsMemberOf ( user . Groups , group . GroupDN ) {
orgRolesMap [ group . OrgId ] = group . OrgRole
u . OrgRoles = append ( u . OrgRoles , LDAPRoleDTO { GroupDN : group . GroupDN ,
OrgId : group . OrgId , OrgRole : group . OrgRole } )
delete ( unmappedUserGroups , strings . ToLower ( group . GroupDN ) )
2022-08-10 03:20:23 -05:00
orgIDs = append ( orgIDs , group . OrgId )
2019-09-03 12:34:44 -05:00
}
}
2022-04-22 08:45:54 -05:00
for userGroup := range unmappedUserGroups {
u . OrgRoles = append ( u . OrgRoles , LDAPRoleDTO { GroupDN : userGroup } )
}
2019-09-03 12:34:44 -05:00
2020-12-15 02:32:06 -06:00
ldapLogger . Debug ( "mapping org roles" , "orgsRoles" , u . OrgRoles )
2022-04-22 08:45:54 -05:00
if err := u . FetchOrgs ( c . Req . Context ( ) , hs . SQLStore ) ; err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "An organization was not found - Please verify your LDAP configuration" , err )
2019-09-03 12:34:44 -05:00
}
2022-08-10 03:20:23 -05:00
u . Teams , err = hs . ldapGroups . GetTeams ( user . Groups , orgIDs )
2022-02-01 05:03:21 -06:00
if err != nil {
2021-01-15 07:43:20 -06:00
return response . Error ( http . StatusBadRequest , "Unable to find the teams for this user" , err )
2019-09-08 05:48:47 -05:00
}
2022-04-15 07:01:58 -05:00
return response . JSON ( http . StatusOK , u )
2019-09-03 12:34:44 -05:00
}
// 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 ]
}
}
2022-07-27 08:54:37 -05:00
// swagger:parameters getUserFromLDAP
type GetLDAPUserParams struct {
// in:path
// required:true
UserName string ` json:"user_name" `
}
// swagger:parameters postSyncUserWithLDAP
type SyncLDAPUserParams struct {
// in:path
// required:true
UserID int64 ` json:"user_id" `
}