2000-05-09 15:43:00 +04:00
/*
Unix SMB / Netbios implementation .
2001-11-28 01:39:57 +03:00
Version 2.2 .
2000-05-09 15:43:00 +04:00
Winbind daemon for ntdom nss module
Copyright ( C ) Tim Potter 2000
2001-11-28 01:39:57 +03:00
Copyright ( C ) Jeremy Allison 2001.
2000-05-09 15:43:00 +04:00
This program is free software ; you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation ; either version 2 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program ; if not , write to the Free Software
Foundation , Inc . , 675 Mass Ave , Cambridge , MA 0213 9 , USA .
*/
# include "winbindd.h"
2001-11-28 01:39:57 +03:00
/***************************************************************
Empty static struct for negative caching .
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
static struct winbindd_gr negative_gr_cache_entry ;
2000-05-09 15:43:00 +04:00
/* Fill a grent structure from various other information */
2001-05-07 08:32:40 +04:00
static BOOL fill_grent ( struct winbindd_gr * gr , char * gr_name ,
gid_t unix_gid )
2000-05-09 15:43:00 +04:00
{
2001-05-07 08:32:40 +04:00
/* Fill in uid/gid */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
gr - > gr_gid = unix_gid ;
/* Group name and password */
safe_strcpy ( gr - > gr_name , gr_name , sizeof ( gr - > gr_name ) - 1 ) ;
safe_strcpy ( gr - > gr_passwd , " x " , sizeof ( gr - > gr_passwd ) - 1 ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
return True ;
2000-05-09 15:43:00 +04:00
}
2001-05-07 08:32:40 +04:00
/* Fill in the group membership field of a NT group given by group_rid */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
static BOOL fill_grent_mem ( struct winbindd_domain * domain ,
uint32 group_rid ,
enum SID_NAME_USE group_name_type ,
int * num_gr_mem , char * * gr_mem , int * gr_mem_len )
2000-05-09 15:43:00 +04:00
{
2001-05-07 08:32:40 +04:00
uint32 * rid_mem = NULL , num_names = 0 ;
2001-09-05 12:11:17 +04:00
uint32 * name_types = NULL ;
2001-05-07 08:32:40 +04:00
int buf_len , buf_ndx , i ;
char * * names = NULL , * buf ;
BOOL result = False ;
2001-11-28 01:39:57 +03:00
TALLOC_CTX * mem_ctx ;
2001-10-19 05:46:43 +04:00
2001-11-28 01:39:57 +03:00
if ( ! ( mem_ctx = talloc_init ( ) ) )
return False ;
2001-10-19 05:46:43 +04:00
2001-05-07 08:32:40 +04:00
/* Initialise group membership information */
DEBUG ( 10 , ( " fill_grent_mem(): group %s rid 0x%x \n " ,
domain ? domain - > name : " NULL " , group_rid ) ) ;
* num_gr_mem = 0 ;
if ( group_name_type ! = SID_NAME_DOM_GRP ) {
DEBUG ( 1 , ( " fill_grent_mem(): rid %d in domain %s isn't a "
" domain group \n " , group_rid , domain - > name ) ) ;
2001-10-19 05:46:43 +04:00
goto done ;
2001-05-07 08:32:40 +04:00
}
/* Lookup group members */
2001-10-19 05:46:43 +04:00
if ( ! winbindd_lookup_groupmem ( domain , mem_ctx , group_rid , & num_names ,
2001-05-07 08:32:40 +04:00
& rid_mem , & names , & name_types ) ) {
DEBUG ( 1 , ( " fill_grent_mem(): could not lookup membership "
" for group rid %d in domain %s \n " ,
group_rid , domain - > name ) ) ;
2001-10-19 05:46:43 +04:00
goto done ;
2001-05-07 08:32:40 +04:00
}
DEBUG ( 10 , ( " fill_grent_mem(): looked up %d names \n " , num_names ) ) ;
if ( DEBUGLEVEL > = 10 ) {
2001-10-19 05:46:43 +04:00
for ( i = 0 ; i < num_names ; i + + )
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " \t %20s %x %d \n " , names [ i ] , rid_mem [ i ] ,
name_types [ i ] ) ) ;
}
/* Add members to list */
buf = NULL ;
buf_len = buf_ndx = 0 ;
again :
for ( i = 0 ; i < num_names ; i + + ) {
char * the_name ;
fstring name ;
int len ;
the_name = names [ i ] ;
2001-10-19 05:46:43 +04:00
DEBUG ( 10 , ( " fill_grent_mem(): processing name %s \n " ,
the_name ) ) ;
2001-05-07 08:32:40 +04:00
2001-10-31 09:20:58 +03:00
/* FIXME: need to cope with groups within groups. These
occur in Universal groups on a Windows 2000 native mode
server . */
2001-05-07 08:32:40 +04:00
if ( name_types [ i ] ! = SID_NAME_USER ) {
DEBUG ( 3 , ( " fill_grent_mem(): name %s isn't a domain "
" user \n " , the_name ) ) ;
continue ;
}
/* Don't bother with machine accounts */
if ( the_name [ strlen ( the_name ) - 1 ] = = ' $ ' ) {
DEBUG ( 10 , ( " fill_grent_mem(): %s is machine account \n " ,
the_name ) ) ;
continue ;
}
/* Append domain name */
snprintf ( name , sizeof ( name ) , " %s%s%s " , domain - > name ,
lp_winbind_separator ( ) , the_name ) ;
len = strlen ( name ) ;
/* Add to list or calculate buffer length */
if ( ! buf ) {
buf_len + = len + 1 ; /* List is comma separated */
( * num_gr_mem ) + + ;
DEBUG ( 10 , ( " fill_grent_mem(): buf_len + %d = %d \n " , len + 1 ,
buf_len ) ) ;
} else {
DEBUG ( 10 , ( " fill_grent_mem(): appending %s at index %d \n " ,
name , len ) ) ;
safe_strcpy ( & buf [ buf_ndx ] , name , len ) ;
buf_ndx + = len ;
buf [ buf_ndx ] = ' , ' ;
buf_ndx + + ;
}
}
/* Allocate buffer */
if ( ! buf ) {
if ( ! ( buf = malloc ( buf_len ) ) ) {
DEBUG ( 1 , ( " fill_grent_mem(): out of memory \n " ) ) ;
result = False ;
2001-10-19 05:46:43 +04:00
goto done ;
2001-05-07 08:32:40 +04:00
}
memset ( buf , 0 , buf_len ) ;
goto again ;
}
if ( buf & & buf_ndx > 0 ) {
buf [ buf_ndx - 1 ] = ' \0 ' ;
}
* gr_mem = buf ;
* gr_mem_len = buf_len ;
2001-11-15 06:28:24 +03:00
DEBUG ( 10 , ( " fill_grent_mem(): num_mem = %d, len = %d, mem = %s \n " ,
* num_gr_mem , buf_len , num_gr_mem ? buf : " NULL " ) ) ;
2001-05-07 08:32:40 +04:00
result = True ;
2001-10-19 05:46:43 +04:00
done :
2001-11-28 01:39:57 +03:00
2001-11-27 09:28:06 +03:00
talloc_destroy ( mem_ctx ) ;
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " fill_grent_mem(): returning %d \n " , result ) ) ;
return result ;
2000-05-09 15:43:00 +04:00
}
2001-05-07 08:32:40 +04:00
/* Return a group structure from a group name */
2000-05-09 15:43:00 +04:00
2001-11-28 01:39:57 +03:00
enum winbindd_result winbindd_getgrnam_from_group ( struct winbindd_cli_state * state )
2001-05-07 08:32:40 +04:00
{
DOM_SID group_sid ;
struct winbindd_domain * domain ;
enum SID_NAME_USE name_type ;
uint32 group_rid ;
fstring name_domain , name_group , name ;
char * tmp , * gr_mem ;
gid_t gid ;
int extra_data_len , gr_mem_len ;
DEBUG ( 3 , ( " [%5d]: getgrnam %s \n " , state - > pid ,
state - > request . data . groupname ) ) ;
/* Parse domain and groupname */
memset ( name_group , 0 , sizeof ( fstring ) ) ;
tmp = state - > request . data . groupname ;
parse_domain_user ( tmp , name_domain , name_group ) ;
/* Reject names that don't have a domain - i.e name_domain contains
the entire name . */
2001-10-11 03:08:13 +04:00
if ( strequal ( name_group , " " ) )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
/* Get info for the domain */
if ( ( domain = find_domain_from_name ( name_domain ) ) = = NULL ) {
DEBUG ( 0 , ( " getgrname_from_group(): could not get domain "
" sid for domain %s \n " , name_domain ) ) ;
return WINBINDD_ERROR ;
}
/* Check for cached group entry */
2001-10-14 12:30:30 +04:00
if ( winbindd_fetch_group_cache_entry ( domain , name_group ,
2001-05-07 08:32:40 +04:00
& state - > response . data . gr ,
& state - > response . extra_data ,
& extra_data_len ) ) {
2001-11-28 01:39:57 +03:00
/* Check if this is a negative cache entry. */
if ( memcmp ( & negative_gr_cache_entry , & state - > response . data . gr ,
sizeof ( state - > response . data . gr ) ) = = 0 )
return WINBINDD_ERROR ;
2001-05-07 08:32:40 +04:00
state - > response . length + = extra_data_len ;
return WINBINDD_OK ;
}
snprintf ( name , sizeof ( name ) , " %s \\ %s " , name_domain , name_group ) ;
/* Get rid and name type from name */
2001-12-03 11:17:46 +03:00
if ( ! winbindd_lookup_sid_by_name ( domain , name , & group_sid , & name_type ) ) {
2001-05-07 08:32:40 +04:00
DEBUG ( 1 , ( " group %s in domain %s does not exist \n " ,
name_group , name_domain ) ) ;
2001-11-28 01:39:57 +03:00
winbindd_store_group_cache_entry ( domain , name_group ,
& negative_gr_cache_entry , NULL , 0 ) ;
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ( name_type ! = SID_NAME_ALIAS ) & & ( name_type ! = SID_NAME_DOM_GRP ) ) {
DEBUG ( 1 , ( " from_group: name '%s' is not a local or domain "
" group: %d \n " , name_group , name_type ) ) ;
2001-11-28 01:39:57 +03:00
winbindd_store_group_cache_entry ( domain , name_group ,
& negative_gr_cache_entry , NULL , 0 ) ;
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Fill in group structure */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
sid_split_rid ( & group_sid , & group_rid ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! winbindd_idmap_get_gid_from_rid ( domain - > name , group_rid , & gid ) ) {
DEBUG ( 1 , ( " error sursing unix gid for sid \n " ) ) ;
2001-11-28 01:39:57 +03:00
winbindd_store_group_cache_entry ( domain , name_group ,
& negative_gr_cache_entry , NULL , 0 ) ;
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! fill_grent ( & state - > response . data . gr ,
state - > request . data . groupname , gid ) | |
! fill_grent_mem ( domain , group_rid , name_type ,
& state - > response . data . gr . num_gr_mem ,
2001-11-28 01:39:57 +03:00
& gr_mem , & gr_mem_len ) ) {
winbindd_store_group_cache_entry ( domain , name_group ,
& negative_gr_cache_entry , NULL , 0 ) ;
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2001-11-28 01:39:57 +03:00
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Group membership lives at start of extra data */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . data . gr . gr_mem_ofs = 0 ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . length + = gr_mem_len ;
state - > response . extra_data = gr_mem ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Update cached group info */
2000-05-09 15:43:00 +04:00
2001-10-14 12:30:30 +04:00
winbindd_store_group_cache_entry ( domain , name_group ,
2001-05-07 08:32:40 +04:00
& state - > response . data . gr ,
state - > response . extra_data ,
gr_mem_len ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
return WINBINDD_OK ;
2000-05-09 15:43:00 +04:00
}
2001-05-07 08:32:40 +04:00
/* Return a group structure from a gid number */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
enum winbindd_result winbindd_getgrnam_from_gid ( struct winbindd_cli_state
* state )
{
struct winbindd_domain * domain ;
DOM_SID group_sid ;
enum SID_NAME_USE name_type ;
fstring group_name ;
uint32 group_rid ;
int extra_data_len , gr_mem_len ;
char * gr_mem ;
2000-05-09 15:43:00 +04:00
2001-10-11 03:08:13 +04:00
DEBUG ( 3 , ( " [%5d]: getgrgid %d \n " , state - > pid ,
state - > request . data . gid ) ) ;
2001-05-07 08:32:40 +04:00
/* Bug out if the gid isn't in the winbind range */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ( state - > request . data . gid < server_state . gid_low ) | |
2001-10-11 03:08:13 +04:00
( state - > request . data . gid > server_state . gid_high ) )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Get rid from gid */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! winbindd_idmap_get_rid_from_gid ( state - > request . data . gid ,
& group_rid , & domain ) ) {
DEBUG ( 1 , ( " Could not convert gid %d to rid \n " ,
state - > request . data . gid ) ) ;
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Try a cached entry */
2000-05-09 15:43:00 +04:00
2001-10-14 12:30:30 +04:00
if ( winbindd_fetch_gid_cache_entry ( domain ,
2001-05-07 08:32:40 +04:00
state - > request . data . gid ,
& state - > response . data . gr ,
& state - > response . extra_data ,
& extra_data_len ) ) {
2001-11-28 01:39:57 +03:00
/* Check if this is a negative cache entry. */
if ( memcmp ( & negative_gr_cache_entry , & state - > response . data . gr ,
sizeof ( state - > response . data . gr ) ) = = 0 )
return WINBINDD_ERROR ;
2001-05-07 08:32:40 +04:00
state - > response . length + = extra_data_len ;
return WINBINDD_OK ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Get sid from gid */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
sid_copy ( & group_sid , & domain - > sid ) ;
sid_append_rid ( & group_sid , group_rid ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! winbindd_lookup_name_by_sid ( & group_sid , group_name , & name_type ) ) {
DEBUG ( 1 , ( " Could not lookup sid \n " ) ) ;
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-10-11 03:08:13 +04:00
if ( strcmp ( lp_winbind_separator ( ) , " \\ " ) )
2001-05-07 08:32:40 +04:00
string_sub ( group_name , " \\ " , lp_winbind_separator ( ) ,
sizeof ( fstring ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! ( ( name_type = = SID_NAME_ALIAS ) | |
( name_type = = SID_NAME_DOM_GRP ) ) ) {
DEBUG ( 1 , ( " from_gid: name '%s' is not a local or domain "
" group: %d \n " , group_name , name_type ) ) ;
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Fill in group structure */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! fill_grent ( & state - > response . data . gr , group_name ,
state - > request . data . gid ) | |
! fill_grent_mem ( domain , group_rid , name_type ,
& state - > response . data . gr . num_gr_mem ,
2001-10-11 03:08:13 +04:00
& gr_mem , & gr_mem_len ) )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Group membership lives at start of extra data */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . data . gr . gr_mem_ofs = 0 ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . length + = gr_mem_len ;
state - > response . extra_data = gr_mem ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Update cached group info */
2000-05-09 15:43:00 +04:00
2001-10-14 12:30:30 +04:00
winbindd_store_gid_cache_entry ( domain , state - > request . data . gid ,
2001-05-07 08:32:40 +04:00
& state - > response . data . gr ,
state - > response . extra_data ,
gr_mem_len ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
return WINBINDD_OK ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/*
* set / get / endgrent functions
*/
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* "Rewind" file pointer for group database enumeration */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
enum winbindd_result winbindd_setgrent ( struct winbindd_cli_state * state )
{
struct winbindd_domain * tmp ;
DEBUG ( 3 , ( " [%5d]: setgrent \n " , state - > pid ) ) ;
/* Check user has enabled this */
2001-10-10 02:55:00 +04:00
if ( ! lp_winbind_enum_groups ( ) )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
/* Free old static data if it exists */
if ( state - > getgrent_state ! = NULL ) {
free_getent_state ( state - > getgrent_state ) ;
state - > getgrent_state = NULL ;
}
/* Create sam pipes for each domain we know about */
2001-11-27 09:28:06 +03:00
if ( domain_list = = NULL )
get_domain_info ( ) ;
2001-11-15 06:28:24 +03:00
2001-05-07 08:32:40 +04:00
for ( tmp = domain_list ; tmp ! = NULL ; tmp = tmp - > next ) {
struct getent_state * domain_state ;
/* Skip domains other than WINBINDD_DOMAIN environment
variable */
if ( ( strcmp ( state - > request . domain , " " ) ! = 0 ) & &
2001-10-10 02:55:00 +04:00
! check_domain_env ( state - > request . domain , tmp - > name ) )
2001-05-07 08:32:40 +04:00
continue ;
/* Create a state record for this domain */
if ( ( domain_state = ( struct getent_state * )
2001-10-10 02:55:00 +04:00
malloc ( sizeof ( struct getent_state ) ) ) = = NULL )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
ZERO_STRUCTP ( domain_state ) ;
2001-10-19 05:46:43 +04:00
domain_state - > domain = tmp ;
2001-05-07 08:32:40 +04:00
/* Add to list of open domains */
DLIST_ADD ( state - > getgrent_state , domain_state ) ;
}
return WINBINDD_OK ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Close file pointer to ntdom group database */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
enum winbindd_result winbindd_endgrent ( struct winbindd_cli_state * state )
{
DEBUG ( 3 , ( " [%5d]: endgrent \n " , state - > pid ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
free_getent_state ( state - > getgrent_state ) ;
state - > getgrent_state = NULL ;
return WINBINDD_OK ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Get the list of domain groups and domain aliases for a domain. We fill in
the sam_entries and num_sam_entries fields with domain group information .
The dispinfo_ndx field is incremented to the index of the next group to
fetch . Return True if some groups were returned , False otherwise . */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
# define MAX_FETCH_SAM_ENTRIES 100
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
static BOOL get_sam_group_entries ( struct getent_state * ent )
{
2001-09-04 11:13:01 +04:00
NTSTATUS status ;
uint32 num_entries ;
2001-10-04 01:10:29 +04:00
struct acct_info * name_list = NULL , * tnl ;
2001-11-28 01:39:57 +03:00
TALLOC_CTX * mem_ctx ;
BOOL result = False ;
2001-05-07 08:32:40 +04:00
2001-10-10 02:55:00 +04:00
if ( ent - > got_all_sam_entries )
2001-05-07 08:32:40 +04:00
return False ;
#if 0
2001-10-14 12:30:30 +04:00
if ( winbindd_fetch_group_cache ( ent - > domain ,
2001-05-07 08:32:40 +04:00
& ent - > sam_entries ,
2001-10-10 02:55:00 +04:00
& ent - > num_sam_entries ) )
2001-05-07 08:32:40 +04:00
return True ;
# endif
2001-10-19 12:22:52 +04:00
2001-11-28 01:39:57 +03:00
if ( ! ( mem_ctx = talloc_init ( ) ) )
return False ;
2001-05-07 08:32:40 +04:00
/* Free any existing group info */
2001-09-17 08:52:45 +04:00
SAFE_FREE ( ent - > sam_entries ) ;
ent - > num_sam_entries = 0 ;
2001-05-07 08:32:40 +04:00
/* Enumerate domain groups */
do {
struct acct_info * sam_grp_entries = NULL ;
num_entries = 0 ;
2001-12-03 04:23:42 +03:00
status = ent - > domain - > methods - > enum_dom_groups ( ent - > domain ,
mem_ctx ,
& ent - > grp_query_start_ndx ,
& num_entries ,
& sam_grp_entries ) ;
2001-11-15 09:55:56 +03:00
2001-12-03 04:23:42 +03:00
if ( ! NT_STATUS_IS_OK ( status ) ) break ;
2001-11-15 09:55:56 +03:00
2001-05-07 08:32:40 +04:00
/* Copy entries into return buffer */
if ( num_entries ) {
2001-10-04 01:10:29 +04:00
tnl = Realloc ( name_list ,
sizeof ( struct acct_info ) *
( ent - > num_sam_entries +
num_entries ) ) ;
2001-10-10 02:55:00 +04:00
if ( tnl = = NULL ) {
DEBUG ( 0 , ( " get_sam_group_entries: unable to "
" realloc a structure! \n " ) ) ;
2001-10-04 01:10:29 +04:00
SAFE_FREE ( name_list ) ;
2001-10-10 02:55:00 +04:00
2001-10-19 12:22:52 +04:00
goto done ;
2001-11-27 09:28:06 +03:00
} else
name_list = tnl ;
2001-05-07 08:32:40 +04:00
memcpy ( & name_list [ ent - > num_sam_entries ] ,
sam_grp_entries ,
num_entries * sizeof ( struct acct_info ) ) ;
}
ent - > num_sam_entries + = num_entries ;
2001-08-30 04:13:55 +04:00
2001-09-04 11:13:01 +04:00
if ( NT_STATUS_V ( status ) ! = NT_STATUS_V ( STATUS_MORE_ENTRIES ) )
2001-05-07 08:32:40 +04:00
break ;
} while ( ent - > num_sam_entries < MAX_FETCH_SAM_ENTRIES ) ;
#if 0
/* Fill cache with received entries */
2001-10-14 12:30:30 +04:00
winbindd_store_group_cache ( ent - > domain , ent - > sam_entries ,
2001-05-07 08:32:40 +04:00
ent - > num_sam_entries ) ;
# endif
/* Fill in remaining fields */
ent - > sam_entries = name_list ;
ent - > sam_entry_index = 0 ;
2001-10-10 02:55:00 +04:00
ent - > got_all_sam_entries = ( NT_STATUS_V ( status ) ! =
NT_STATUS_V ( STATUS_MORE_ENTRIES ) ) ;
2001-05-07 08:32:40 +04:00
2001-10-19 12:22:52 +04:00
result = ( ent - > num_sam_entries > 0 ) ;
done :
2001-11-28 01:39:57 +03:00
talloc_destroy ( mem_ctx ) ;
return result ;
2001-05-07 08:32:40 +04:00
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Fetch next group entry from ntdom database */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
# define MAX_GETGRENT_GROUPS 500
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
enum winbindd_result winbindd_getgrent ( struct winbindd_cli_state * state )
{
struct getent_state * ent ;
struct winbindd_gr * group_list = NULL ;
int num_groups , group_list_ndx = 0 , i , gr_mem_list_len = 0 ;
char * sep , * new_extra_data , * gr_mem_list = NULL ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 3 , ( " [%5d]: getgrent \n " , state - > pid ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Check user has enabled this */
2000-05-09 15:43:00 +04:00
2001-10-10 02:55:00 +04:00
if ( ! lp_winbind_enum_groups ( ) )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
num_groups = MIN ( MAX_GETGRENT_GROUPS , state - > request . data . num_entries ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ( state - > response . extra_data =
2001-10-10 02:55:00 +04:00
malloc ( num_groups * sizeof ( struct winbindd_gr ) ) ) = = NULL )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . data . num_entries = 0 ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
group_list = ( struct winbindd_gr * ) state - > response . extra_data ;
sep = lp_winbind_separator ( ) ;
2000-05-09 15:43:00 +04:00
2001-10-10 02:55:00 +04:00
if ( ! ( ent = state - > getgrent_state ) )
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Start sending back groups */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
for ( i = 0 ; i < num_groups ; i + + ) {
struct acct_info * name_list = NULL ;
fstring domain_group_name ;
uint32 result ;
gid_t group_gid ;
/* Do we need to fetch another chunk of groups? */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
tryagain :
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " getgrent(): entry_index = %d, num_entries = %d \n " ,
ent - > sam_entry_index , ent - > num_sam_entries ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ent - > num_sam_entries = = ent - > sam_entry_index ) {
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
while ( ent & & ! get_sam_group_entries ( ent ) ) {
struct getent_state * next_ent ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " getgrent(): freeing state info for "
2001-08-30 04:13:55 +04:00
" domain %s \n " , ent - > domain - > name ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Free state information for this domain */
2000-05-09 15:43:00 +04:00
2001-09-17 08:52:45 +04:00
SAFE_FREE ( ent - > sam_entries ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
next_ent = ent - > next ;
DLIST_REMOVE ( state - > getgrent_state , ent ) ;
2001-09-17 08:52:45 +04:00
SAFE_FREE ( ent ) ;
2001-05-07 08:32:40 +04:00
ent = next_ent ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* No more domains */
2000-05-09 15:43:00 +04:00
2001-10-10 02:55:00 +04:00
if ( ! ent )
break ;
2001-05-07 08:32:40 +04:00
}
name_list = ent - > sam_entries ;
/* Lookup group info */
if ( ! winbindd_idmap_get_gid_from_rid (
ent - > domain - > name ,
name_list [ ent - > sam_entry_index ] . rid ,
& group_gid ) ) {
DEBUG ( 1 , ( " getgrent(): could not look up gid for group %s \n " ,
name_list [ ent - > sam_entry_index ] . acct_name ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
ent - > sam_entry_index + + ;
goto tryagain ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " getgrent(): got gid %d for group %x \n " , group_gid ,
name_list [ ent - > sam_entry_index ] . rid ) ) ;
/* Fill in group entry */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
slprintf ( domain_group_name , sizeof ( domain_group_name ) - 1 ,
" %s%s%s " , ent - > domain - > name , lp_winbind_separator ( ) ,
name_list [ ent - > sam_entry_index ] . acct_name ) ;
result = fill_grent ( & group_list [ group_list_ndx ] ,
domain_group_name , group_gid ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Fill in group membership entry */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( result ) {
int gr_mem_len ;
char * gr_mem , * new_gr_mem_list ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Get group membership */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
result = fill_grent_mem (
ent - > domain ,
name_list [ ent - > sam_entry_index ] . rid ,
SID_NAME_DOM_GRP ,
& group_list [ group_list_ndx ] . num_gr_mem ,
& gr_mem , & gr_mem_len ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Append to group membership list */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
new_gr_mem_list = Realloc (
gr_mem_list ,
gr_mem_list_len + gr_mem_len ) ;
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
if ( ! new_gr_mem_list & & ( group_list [ group_list_ndx ] . num_gr_mem ! = 0 ) ) {
2001-05-07 08:32:40 +04:00
DEBUG ( 0 , ( " getgrent(): out of memory \n " ) ) ;
2001-09-17 08:52:45 +04:00
SAFE_FREE ( gr_mem_list ) ;
2001-05-07 08:32:40 +04:00
gr_mem_list_len = 0 ;
break ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " getgrent(): list_len = %d, mem_len = %d \n " ,
gr_mem_list_len , gr_mem_len ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
gr_mem_list = new_gr_mem_list ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
memcpy ( & gr_mem_list [ gr_mem_list_len ] , gr_mem ,
gr_mem_len ) ;
2000-05-09 15:43:00 +04:00
2001-09-17 08:52:45 +04:00
SAFE_FREE ( gr_mem ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
group_list [ group_list_ndx ] . gr_mem_ofs =
gr_mem_list_len ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
gr_mem_list_len + = gr_mem_len ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
ent - > sam_entry_index + + ;
/* Add group to return list */
if ( result ) {
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " getgrent(): adding group num_entries = %d \n " ,
state - > response . data . num_entries ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
group_list_ndx + + ;
state - > response . data . num_entries + + ;
state - > response . length + =
sizeof ( struct winbindd_gr ) ;
} else {
DEBUG ( 0 , ( " could not lookup domain group %s \n " ,
domain_group_name ) ) ;
}
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Copy the list of group memberships to the end of the extra data */
2000-05-09 15:43:00 +04:00
2001-10-10 02:55:00 +04:00
if ( group_list_ndx = = 0 )
2001-05-07 08:32:40 +04:00
goto done ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
new_extra_data = Realloc (
state - > response . extra_data ,
group_list_ndx * sizeof ( struct winbindd_gr ) + gr_mem_list_len ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ! new_extra_data ) {
DEBUG ( 0 , ( " out of memory \n " ) ) ;
group_list_ndx = 0 ;
2001-09-17 08:52:45 +04:00
SAFE_FREE ( state - > response . extra_data ) ;
SAFE_FREE ( gr_mem_list ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . extra_data = new_extra_data ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
memcpy ( & ( ( char * ) state - > response . extra_data )
[ group_list_ndx * sizeof ( struct winbindd_gr ) ] ,
gr_mem_list , gr_mem_list_len ) ;
2000-05-09 15:43:00 +04:00
2001-09-17 08:52:45 +04:00
SAFE_FREE ( gr_mem_list ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
state - > response . length + = gr_mem_list_len ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 10 , ( " getgrent(): returning %d groups, length = %d \n " ,
group_list_ndx , gr_mem_list_len ) ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Out of domains */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
done :
2001-11-28 01:39:57 +03:00
2001-05-07 08:32:40 +04:00
return ( group_list_ndx > 0 ) ? WINBINDD_OK : WINBINDD_ERROR ;
2000-05-09 15:43:00 +04:00
}
2001-05-07 08:32:40 +04:00
/* List domain groups without mapping to unix ids */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
enum winbindd_result winbindd_list_groups ( struct winbindd_cli_state * state )
2000-05-09 15:43:00 +04:00
{
2001-11-28 01:39:57 +03:00
uint32 total_entries = 0 ;
2001-08-30 04:13:55 +04:00
uint32 num_domain_entries ;
2001-11-28 01:39:57 +03:00
struct winbindd_domain * domain ;
2001-05-07 08:32:40 +04:00
struct getent_state groups ;
2001-08-30 04:13:55 +04:00
char * extra_data = NULL ;
char * ted = NULL ;
2001-05-07 08:32:40 +04:00
int extra_data_len = 0 , i ;
2001-08-30 04:13:55 +04:00
void * sam_entries = NULL ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
DEBUG ( 3 , ( " [%5d]: list groups \n " , state - > pid ) ) ;
2000-05-09 15:43:00 +04:00
2001-11-28 01:39:57 +03:00
/* Enumerate over trusted domains */
2001-10-10 02:55:00 +04:00
2001-08-30 04:13:55 +04:00
ZERO_STRUCT ( groups ) ;
2001-10-10 02:55:00 +04:00
2001-11-28 01:39:57 +03:00
if ( domain_list = = NULL )
get_domain_info ( ) ;
2001-11-15 06:28:24 +03:00
2001-11-28 01:39:57 +03:00
for ( domain = domain_list ; domain ; domain = domain - > next ) {
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Skip domains other than WINBINDD_DOMAIN environment
variable */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
if ( ( strcmp ( state - > request . domain , " " ) ! = 0 ) & &
2001-10-10 02:55:00 +04:00
! check_domain_env ( state - > request . domain , domain - > name ) )
2001-05-07 08:32:40 +04:00
continue ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Get list of sam groups */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
ZERO_STRUCT ( groups ) ;
groups . domain = domain ;
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
/*
* iterate through all groups
2001-10-10 02:55:00 +04:00
* total_entries : maintains a total count over * * all domains * *
* num_domain_entries : is the running count for this domain
2001-08-30 04:13:55 +04:00
*/
num_domain_entries = 0 ;
2001-10-10 02:55:00 +04:00
while ( get_sam_group_entries ( & groups ) ) {
2001-08-30 04:13:55 +04:00
int new_size ;
int offset ;
offset = sizeof ( struct acct_info ) * num_domain_entries ;
new_size = sizeof ( struct acct_info )
* ( groups . num_sam_entries + num_domain_entries ) ;
sam_entries = Realloc ( sam_entries , new_size ) ;
if ( ! sam_entries )
return WINBINDD_ERROR ;
num_domain_entries + = groups . num_sam_entries ;
2001-10-10 02:55:00 +04:00
memcpy ( ( ( char * ) sam_entries ) + offset ,
groups . sam_entries ,
sizeof ( struct acct_info ) *
groups . num_sam_entries ) ;
2001-08-30 04:13:55 +04:00
groups . sam_entries = NULL ;
groups . num_sam_entries = 0 ;
}
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
/* skip remainder of loop if we idn;t retrieve any groups */
if ( num_domain_entries = = 0 )
2001-10-21 11:01:01 +04:00
continue ;
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
/* setup the groups struct to contain all the groups
retrieved for this domain */
groups . num_sam_entries = num_domain_entries ;
groups . sam_entries = sam_entries ;
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
/* keep track the of the total number of groups seen so
far over all domains */
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
total_entries + = groups . num_sam_entries ;
/* Allocate some memory for extra data. Note that we limit
account names to sizeof ( fstring ) = 128 characters . */
ted = Realloc ( extra_data , sizeof ( fstring ) * total_entries ) ;
2001-08-12 21:30:01 +04:00
if ( ! ted ) {
2001-10-10 02:55:00 +04:00
DEBUG ( 0 , ( " winbindd_list_groups: failed to enlarge "
" buffer! \n " ) ) ;
2001-09-17 08:52:45 +04:00
SAFE_FREE ( extra_data ) ;
2001-05-07 08:32:40 +04:00
return WINBINDD_ERROR ;
2001-08-30 04:13:55 +04:00
} else
extra_data = ted ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Pack group list into extra data fields */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
for ( i = 0 ; i < groups . num_sam_entries ; i + + ) {
char * group_name = ( ( struct acct_info * )
groups . sam_entries ) [ i ] . acct_name ;
fstring name ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Convert unistring to ascii */
2000-05-09 15:43:00 +04:00
2001-08-30 04:13:55 +04:00
snprintf ( name , sizeof ( name ) , " %s%s%s " , domain - > name ,
lp_winbind_separator ( ) , group_name ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Append to extra data */
2001-10-10 02:55:00 +04:00
memcpy ( & extra_data [ extra_data_len ] , name ,
strlen ( name ) ) ;
2001-05-07 08:32:40 +04:00
extra_data_len + = strlen ( name ) ;
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
extra_data [ extra_data_len + + ] = ' , ' ;
}
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* Assign extra_data fields in response structure */
if ( extra_data ) {
extra_data [ extra_data_len - 1 ] = ' \0 ' ;
state - > response . extra_data = extra_data ;
state - > response . length + = extra_data_len ;
}
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
/* No domains may have responded but that's still OK so don't
return an error . */
2000-05-09 15:43:00 +04:00
2001-11-28 01:39:57 +03:00
return WINBINDD_OK ;
2000-05-09 15:43:00 +04:00
}
2001-05-07 08:32:40 +04:00
/* Get user supplementary groups. This is much quicker than trying to
invert the groups database . */
2000-05-09 15:43:00 +04:00
2001-05-07 08:32:40 +04:00
enum winbindd_result winbindd_getgroups ( struct winbindd_cli_state * state )
2000-05-09 15:43:00 +04:00
{
2001-05-07 08:32:40 +04:00
fstring name_domain , name_user , name ;
DOM_SID user_sid ;
enum SID_NAME_USE name_type ;
uint32 user_rid , num_groups , num_gids ;
DOM_GID * user_groups = NULL ;
struct winbindd_domain * domain ;
2001-11-26 04:20:57 +03:00
enum winbindd_result result = WINBINDD_ERROR ;
2001-05-07 08:32:40 +04:00
gid_t * gid_list ;
int i ;
2001-11-28 01:39:57 +03:00
TALLOC_CTX * mem_ctx ;
2001-05-07 08:32:40 +04:00
DEBUG ( 3 , ( " [%5d]: getgroups %s \n " , state - > pid ,
state - > request . data . username ) ) ;
2001-11-28 01:39:57 +03:00
if ( ! ( mem_ctx = talloc_init ( ) ) )
return WINBINDD_ERROR ;
2001-11-26 04:20:57 +03:00
2001-05-07 08:32:40 +04:00
/* Parse domain and username */
parse_domain_user ( state - > request . data . username , name_domain ,
name_user ) ;
/* Reject names that don't have a domain - i.e name_domain contains
the entire name . */
2001-10-10 02:55:00 +04:00
if ( strequal ( name_domain , " " ) )
2001-11-26 04:20:57 +03:00
goto done ;
2001-05-07 08:32:40 +04:00
/* Get info for the domain */
if ( ( domain = find_domain_from_name ( name_domain ) ) = = NULL ) {
DEBUG ( 0 , ( " could not find domain entry for domain %s \n " ,
name_domain ) ) ;
2001-11-26 04:20:57 +03:00
goto done ;
2001-05-07 08:32:40 +04:00
}
slprintf ( name , sizeof ( name ) - 1 , " %s \\ %s " , name_domain , name_user ) ;
/* Get rid and name type from name. The following costs 1 packet */
2001-12-03 11:17:46 +03:00
if ( ! winbindd_lookup_sid_by_name ( domain , name , & user_sid , & name_type ) ) {
2001-05-07 08:32:40 +04:00
DEBUG ( 1 , ( " user '%s' does not exist \n " , name_user ) ) ;
2001-11-28 01:39:57 +03:00
goto done ;
2001-05-07 08:32:40 +04:00
}
if ( name_type ! = SID_NAME_USER ) {
DEBUG ( 1 , ( " name '%s' is not a user name: %d \n " , name_user ,
name_type ) ) ;
2001-11-28 01:39:57 +03:00
goto done ;
2001-05-07 08:32:40 +04:00
}
sid_split_rid ( & user_sid , & user_rid ) ;
2001-11-26 04:20:57 +03:00
if ( ! winbindd_lookup_usergroups ( domain , mem_ctx , user_rid ,
& num_groups , & user_groups ) )
2001-11-28 01:39:57 +03:00
goto done ;
2001-05-07 08:32:40 +04:00
/* Copy data back to client */
num_gids = 0 ;
gid_list = malloc ( sizeof ( gid_t ) * num_groups ) ;
2001-11-28 01:39:57 +03:00
if ( state - > response . extra_data )
2001-05-07 08:32:40 +04:00
goto done ;
for ( i = 0 ; i < num_groups ; i + + ) {
if ( ! winbindd_idmap_get_gid_from_rid (
domain - > name , user_groups [ i ] . g_rid ,
& gid_list [ num_gids ] ) ) {
DEBUG ( 1 , ( " unable to convert group rid %d to gid \n " ,
user_groups [ i ] . g_rid ) ) ;
continue ;
}
num_gids + + ;
}
state - > response . data . num_entries = num_gids ;
state - > response . extra_data = gid_list ;
state - > response . length + = num_gids * sizeof ( gid_t ) ;
result = WINBINDD_OK ;
done :
2001-11-28 01:39:57 +03:00
talloc_destroy ( mem_ctx ) ;
2001-05-07 08:32:40 +04:00
return result ;
2000-05-09 15:43:00 +04:00
}