1#
2# Copyright (c) 2009-2015, Ashok P. Nadkarni
3# All rights reserved.
4#
5# See the file LICENSE for license
6
7package require twapi_security
8
9namespace eval twapi {
10    record USER_INFO_0 {-name}
11    record USER_INFO_1 [concat [USER_INFO_0] {
12        -password -password_age -priv -home_dir -comment -flags -script_path
13    }]
14    record USER_INFO_2 [concat [USER_INFO_1] {
15        -auth_flags -full_name -usr_comment -parms
16        -workstations -last_logon -last_logoff -acct_expires -max_storage
17        -units_per_week -logon_hours -bad_pw_count -num_logons
18        -logon_server -country_code -code_page
19    }]
20    record USER_INFO_3 [concat [USER_INFO_2] {
21        -user_id -primary_group_id -profile -home_dir_drive -password_expired
22    }]
23    record USER_INFO_4 [concat [USER_INFO_2] {
24        -sid -primary_group_id -profile -home_dir_drive -password_expired
25    }]
26
27    record GROUP_INFO_0 {-name}
28    record GROUP_INFO_1 {-name -comment}
29    record GROUP_INFO_2 {-name -comment -group_id -attributes}
30    record GROUP_INFO_3 {-name -comment -sid -attributes}
31
32    record NetEnumResult {moredata hresume totalentries entries}
33
34}
35
36# Add a new user account
37proc twapi::new_user {username args} {
38    array set opts [parseargs args [list \
39                                        system.arg \
40                                        password.arg \
41                                        comment.arg \
42                                        [list priv.arg "user" [array names twapi::priv_level_map]] \
43                                        home_dir.arg \
44                                        script_path.arg \
45                                       ] \
46                        -nulldefault]
47
48    if {$opts(priv) ne "user"} {
49        error "Option -priv is deprecated and values other than 'user' are not allowed"
50    }
51
52    # 1 -> priv level 'user'. NetUserAdd mandates this as only allowed value
53    NetUserAdd $opts(system) $username $opts(password) 1 \
54        $opts(home_dir) $opts(comment) 0 $opts(script_path)
55
56
57    # Backward compatibility - add to 'Users' local group
58    # but only if -system is local
59    if {$opts(system) eq "" ||
60        ([info exists ::env(COMPUTERNAME)] &&
61         [string equal -nocase $opts(system) $::env(COMPUTERNAME)])} {
62        trap {
63            _set_user_priv_level $username $opts(priv) -system $opts(system)
64        } onerror {} {
65            # Remove the previously created user account
66            catch {delete_user $username -system $opts(system)}
67            rethrow
68        }
69    }
70}
71
72
73# Delete a user account
74proc twapi::delete_user {username args} {
75    array set opts [parseargs args {system.arg} -nulldefault]
76
77    # Remove the user from the LSA rights database.
78    _delete_rights $username $opts(system)
79
80    NetUserDel $opts(system) $username
81}
82
83
84# Define various functions to set various user account fields
85foreach twapi::_field_ {
86    {name  0}
87    {password  1003}
88    {home_dir  1006}
89    {comment  1007}
90    {script_path  1009}
91    {full_name  1011}
92    {country_code  1024}
93    {profile  1052}
94    {home_dir_drive  1053}
95} {
96    proc twapi::set_user_[lindex $::twapi::_field_ 0] {username fieldval args} "
97        array set opts \[parseargs args {
98            system.arg
99        } -nulldefault \]
100        Twapi_NetUserSetInfo [lindex $::twapi::_field_ 1] \$opts(system) \$username \$fieldval"
101}
102unset twapi::_field_
103
104# Set account expiry time
105proc twapi::set_user_expiration {username time args} {
106    array set opts [parseargs args {system.arg} -nulldefault]
107
108    if {![string is integer -strict $time]} {
109        if {[string equal $time "never"]} {
110            set time -1
111        } else {
112            set time [clock scan $time]
113        }
114    }
115    Twapi_NetUserSetInfo 1017 $opts(system) $username $time
116}
117
118# Unlock a user account
119proc twapi::unlock_user {username args} {
120    # UF_LOCKOUT -> 0x10
121    _change_user_info_flags $username 0x10 0 {*}$args
122}
123
124# Enable a user account
125proc twapi::enable_user {username args} {
126    # UF_ACCOUNTDISABLE -> 0x2
127    _change_user_info_flags $username 0x2 0 {*}$args
128}
129
130# Disable a user account
131proc twapi::disable_user {username args} {
132    # UF_ACCOUNTDISABLE -> 0x2
133    _change_user_info_flags $username 0x2 0x2 {*}$args
134}
135
136
137# Return the specified fields for a user account
138proc twapi::get_user_account_info {account args} {
139    # Define each option, the corresponding field, and the
140    # information level at which it is returned
141    array set fields {
142        comment 1
143        password_expired 4
144        full_name 2
145        parms 2
146        units_per_week 2
147        primary_group_id 4
148        flags 1
149        logon_server 2
150        country_code 2
151        home_dir 1
152        password_age 1
153        home_dir_drive 4
154        num_logons 2
155        acct_expires 2
156        last_logon 2
157        usr_comment 2
158        bad_pw_count 2
159        code_page 2
160        logon_hours 2
161        workstations 2
162        last_logoff 2
163        name 0
164        script_path 1
165        profile 4
166        max_storage 2
167    }
168    # Left out - auth_flags 2
169    # Left out (always returned as NULL) - password {usri3_password 1}
170    # Note sid is available at level 4 as well but don't want to set
171    # level 4 just for that since we can get it by other means. Hence
172    # not listed above
173
174    array set opts [parseargs args \
175                        [concat [array names fields] sid \
176                             internet_identity \
177                             status type password_attrs \
178                             [list local_groups global_groups system.arg all]] \
179                        -nulldefault]
180
181    if {$opts(all)} {
182        set level 4
183        set opts(local_groups) 1
184        set opts(global_groups) 1
185    } else {
186        # Based on specified fields, figure out what level info to ask for
187        set level -1
188        foreach {opt optval} [array get opts] {
189            if {[info exists fields($opt)] &&
190                $optval &&
191                $fields($opt) > $level
192            } {
193                set level $fields($opt)
194            }
195        }
196        if {$opts(status) || $opts(type) || $opts(password_attrs)} {
197            # These fields are based on the flags field
198            if {$level < 1} {
199                set level 1
200            }
201        }
202    }
203
204    array set result [list ]
205
206    if {$level > -1} {
207        set rawdata [NetUserGetInfo $opts(system) $account $level]
208        array set data [USER_INFO_$level $rawdata]
209
210        # Extract the requested data
211        foreach opt [array names fields] {
212            if {$opts(all) || $opts($opt)} {
213                set result(-$opt) $data(-$opt)
214            }
215        }
216        if {$level == 4 && ($opts(all) || $opts(sid))} {
217            set result(-sid) $data(-sid)
218        }
219
220        # Map internal values to more friendly formats
221        if {$opts(all) || $opts(status) || $opts(type) || $opts(password_attrs)} {
222            array set result [_map_userinfo_flags $data(-flags)]
223            if {! $opts(all)} {
224                if {! $opts(status)} {unset result(-status)}
225                if {! $opts(type)} {unset result(-type)}
226                if {! $opts(password_attrs)} {unset result(-password_attrs)}
227            }
228        }
229
230        if {[info exists result(-logon_hours)]} {
231            binary scan $result(-logon_hours) b* result(-logon_hours)
232        }
233
234        foreach time_field {-acct_expires -last_logon -last_logoff} {
235            if {[info exists result($time_field)]} {
236                if {$result($time_field) == -1 || $result($time_field) == 4294967295} {
237                    set result($time_field) "never"
238                } elseif {$result($time_field) == 0} {
239                    set result($time_field) "unknown"
240                }
241            }
242        }
243    }
244
245    if {$opts(all) || $opts(internet_identity)} {
246        set result(-internet_identity) {}
247        if {[min_os_version 6 2]} {
248            set inet_ident [NetUserGetInfo $opts(system) $account 24]
249            if {[llength $inet_ident]} {
250                set result(-internet_identity) [twine {
251                    internet_provider_name internet_principal_name sid
252                } [lrange $inet_ident 1 end]]
253            }
254        }
255    }
256
257    # The Net* calls always return structures as lists even when the struct
258    # contains only one field so we need to lpick to extract the field
259
260    if {$opts(local_groups)} {
261        set result(-local_groups) [lpick [NetEnumResult entries [NetUserGetLocalGroups $opts(system) $account 0 0]] 0]
262    }
263
264    if {$opts(global_groups)} {
265        set result(-global_groups) [lpick [NetEnumResult entries [NetUserGetGroups $opts(system) $account 0]] 0]
266    }
267
268    if {$opts(sid)  && ! [info exists result(-sid)]} {
269        set result(-sid) [lookup_account_name $account -system $opts(system)]
270    }
271
272    return [array get result]
273}
274
275proc twapi::get_user_global_groups {account args} {
276    parseargs args {
277        system.arg
278        denyonly
279        all
280    } -nulldefault -maxleftover 0 -setvars
281
282    set groups {}
283    foreach elem [NetEnumResult entries [NetUserGetGroups $system [map_account_to_name $account -system $system] 1]] {
284        # 0x10 -> SE_GROUP_USE_FOR_DENY_ONLY
285        set marked_denyonly [expr {[lindex $elem 1] & 0x10}]
286        if {$all || ($denyonly && $marked_denyonly) || !($denyonly || $marked_denyonly)} {
287            lappend groups [lindex $elem 0]
288        }
289    }
290    return $groups
291}
292
293proc twapi::get_user_local_groups {account args} {
294    parseargs args {
295        system.arg
296        {recurse.bool 0}
297    } -nulldefault -maxleftover 0 -setvars
298
299    # The Net* calls always return structures as lists even when the struct
300    # contains only one field so we need to lpick to extract the field
301    return [lpick [NetEnumResult entries [NetUserGetLocalGroups $system [map_account_to_name $account -system $system] 0 $recurse]] 0]
302}
303
304proc twapi::get_user_local_groups_recursive {account args} {
305    return [get_user_local_groups $account {*}$args -recurse 1]
306}
307
308
309# Set the specified fields for a user account
310proc twapi::set_user_account_info {account args} {
311
312    # Define each option, the corresponding field, and the
313    # information level at which it is returned
314    array set opts [parseargs args {
315        {system.arg ""}
316        comment.arg
317        full_name.arg
318        country_code.arg
319        home_dir.arg
320        home_dir.arg
321        acct_expires.arg
322        name.arg
323        script_path.arg
324        profile.arg
325    }]
326
327    # TBD - rewrite this to be atomic
328
329    if {[info exists opts(comment)]} {
330        set_user_comment $account $opts(comment) -system $opts(system)
331    }
332
333    if {[info exists opts(full_name)]} {
334        set_user_full_name $account $opts(full_name) -system $opts(system)
335    }
336
337    if {[info exists opts(country_code)]} {
338        set_user_country_code $account $opts(country_code) -system $opts(system)
339    }
340
341    if {[info exists opts(home_dir)]} {
342        set_user_home_dir $account $opts(home_dir) -system $opts(system)
343    }
344
345    if {[info exists opts(home_dir_drive)]} {
346        set_user_home_dir_drive $account $opts(home_dir_drive) -system $opts(system)
347    }
348
349    if {[info exists opts(acct_expires)]} {
350        set_user_expiration $account $opts(acct_expires) -system $opts(system)
351    }
352
353    if {[info exists opts(name)]} {
354        set_user_name $account $opts(name) -system $opts(system)
355    }
356
357    if {[info exists opts(script_path)]} {
358        set_user_script_path $account $opts(script_path) -system $opts(system)
359    }
360
361    if {[info exists opts(profile)]} {
362        set_user_profile $account $opts(profile) -system $opts(system)
363    }
364}
365
366
367proc twapi::get_global_group_info {grpname args} {
368    array set opts [parseargs args {
369        {system.arg ""}
370        comment
371        name
372        members
373        sid
374        attributes
375        all
376    } -maxleftover 0]
377
378    set result {}
379    if {[expr {$opts(comment) || $opts(name) || $opts(sid) || $opts(attributes) || $opts(all)}]} {
380        # 3 -> GROUP_INFO level 3
381        lassign [NetGroupGetInfo $opts(system) $grpname 3] name comment sid attributes
382        if {$opts(all) || $opts(sid)} {
383            lappend result -sid $sid
384        }
385        if {$opts(all) || $opts(name)} {
386            lappend result -name $name
387        }
388        if {$opts(all) || $opts(comment)} {
389            lappend result -comment $comment
390        }
391        if {$opts(all) || $opts(attributes)} {
392            lappend result -attributes [map_token_group_attr $attributes]
393        }
394    }
395
396    if {$opts(all) || $opts(members)} {
397        lappend result -members [get_global_group_members $grpname -system $opts(system)]
398    }
399
400    return $result
401}
402
403# Get info about a local or global group
404proc twapi::get_local_group_info {name args} {
405    array set opts [parseargs args {
406        {system.arg ""}
407        comment
408        name
409        members
410        sid
411        all
412    } -maxleftover 0]
413
414    set result [list ]
415    if {$opts(all) || $opts(sid)} {
416        lappend result -sid [lookup_account_name $name -system $opts(system)]
417    }
418    if {$opts(all) || $opts(comment) || $opts(name)} {
419        lassign [NetLocalGroupGetInfo $opts(system) $name 1] name comment
420        if {$opts(all) || $opts(name)} {
421            lappend result -name $name
422        }
423        if {$opts(all) || $opts(comment)} {
424            lappend result -comment $comment
425        }
426    }
427    if {$opts(all) || $opts(members)} {
428        lappend result -members [get_local_group_members $name -system $opts(system)]
429    }
430    return $result
431}
432
433# Get list of users on a system
434proc twapi::get_users {args} {
435    parseargs args {
436        level.int
437    } -setvars -ignoreunknown
438
439    # TBD -allow user to specify filter
440    lappend args -filter 0
441    if {[info exists level]} {
442        lappend args -level $level -fields [USER_INFO_$level]
443    }
444    return [_net_enum_helper NetUserEnum $args]
445}
446
447proc twapi::get_global_groups {args} {
448    parseargs args {
449        level.int
450    } -setvars -ignoreunknown
451
452    # TBD - level 3 returns an ERROR_INVALID_LEVEL even though
453    # MSDN says its valid for NetGroupEnum
454
455    if {[info exists level]} {
456        lappend args -level $level -fields [GROUP_INFO_$level]
457    }
458    return [_net_enum_helper NetGroupEnum $args]
459}
460
461proc twapi::get_local_groups {args} {
462    parseargs args {
463        level.int
464    } -setvars -ignoreunknown
465
466    if {[info exists level]} {
467        lappend args -level $level -fields [dict get {0 {-name} 1 {-name -comment}} $level]
468    }
469    return [_net_enum_helper NetLocalGroupEnum $args]
470}
471
472# Create a new global group
473proc twapi::new_global_group {grpname args} {
474    array set opts [parseargs args {
475        system.arg
476        comment.arg
477    } -nulldefault]
478
479    NetGroupAdd $opts(system) $grpname $opts(comment)
480}
481
482# Create a new local group
483proc twapi::new_local_group {grpname args} {
484    array set opts [parseargs args {
485        system.arg
486        comment.arg
487    } -nulldefault]
488
489    NetLocalGroupAdd $opts(system) $grpname $opts(comment)
490}
491
492
493# Delete a global group
494proc twapi::delete_global_group {grpname args} {
495    array set opts [parseargs args {system.arg} -nulldefault]
496
497    # Remove the group from the LSA rights database.
498    _delete_rights $grpname $opts(system)
499
500    NetGroupDel $opts(system) $grpname
501}
502
503# Delete a local group
504proc twapi::delete_local_group {grpname args} {
505    array set opts [parseargs args {system.arg} -nulldefault]
506
507    # Remove the group from the LSA rights database.
508    _delete_rights $grpname $opts(system)
509
510    NetLocalGroupDel $opts(system) $grpname
511}
512
513
514# Enumerate members of a global group
515proc twapi::get_global_group_members {grpname args} {
516    parseargs args {
517        level.int
518    } -setvars -ignoreunknown
519
520    if {[info exists level]} {
521        lappend args -level $level -fields [dict! {0 {-name} 1 {-name -attributes}} $level]
522    }
523
524    lappend args -preargs [list $grpname] -namelevel 1
525    return [_net_enum_helper NetGroupGetUsers $args]
526}
527
528# Enumerate members of a local group
529proc twapi::get_local_group_members {grpname args} {
530    parseargs args {
531        level.int
532    } -setvars -ignoreunknown
533
534    if {[info exists level]} {
535        lappend args -level $level -fields [dict! {0 {-sid} 1 {-sid -sidusage -name} 2 {-sid -sidusage -domainandname} 3 {-domainandname}} $level]
536    }
537
538    lappend args -preargs [list $grpname] -namelevel 1 -namefield 2
539    return [_net_enum_helper NetLocalGroupGetMembers $args]
540}
541
542# Add a user to a global group
543proc twapi::add_user_to_global_group {grpname username args} {
544    array set opts [parseargs args {system.arg} -nulldefault]
545
546    # No error if already member of the group
547    trap {
548        NetGroupAddUser $opts(system) $grpname $username
549    } onerror {TWAPI_WIN32 1320} {
550        # Ignore
551    }
552}
553
554
555# Remove a user from a global group
556proc twapi::remove_user_from_global_group {grpname username args} {
557    array set opts [parseargs args {system.arg} -nulldefault]
558
559    trap {
560        NetGroupDelUser $opts(system) $grpname $username
561    } onerror {TWAPI_WIN32 1321} {
562        # Was not in group - ignore
563    }
564}
565
566
567# Add a user to a local group
568proc twapi::add_member_to_local_group {grpname username args} {
569    array set opts [parseargs args {
570        system.arg
571        {type.arg name}
572    } -nulldefault]
573
574    # No error if already member of the group
575    trap {
576        Twapi_NetLocalGroupMembers 0 $opts(system) $grpname [expr {$opts(type) eq "sid" ? 0 : 3}] [list $username]
577    } onerror {TWAPI_WIN32 1378} {
578        # Ignore
579    }
580}
581
582proc twapi::add_members_to_local_group {grpname accts args} {
583    array set opts [parseargs args {
584        system.arg
585        {type.arg name}
586    } -nulldefault]
587
588    Twapi_NetLocalGroupMembers 0 $opts(system) $grpname [expr {$opts(type) eq "sid" ? 0 : 3}] $accts
589}
590
591
592# Remove a user from a local group
593proc twapi::remove_member_from_local_group {grpname username args} {
594    array set opts [parseargs args {
595        system.arg
596        {type.arg name}
597    } -nulldefault]
598
599    trap {
600        Twapi_NetLocalGroupMembers 1 $opts(system) $grpname [expr {$opts(type) eq "sid" ? 0 : 3}] [list $username]
601    } onerror {TWAPI_WIN32 1377} {
602        # Was not in group - ignore
603    }
604}
605
606proc twapi::remove_members_from_local_group {grpname accts args} {
607    array set opts [parseargs args {
608        system.arg
609        {type.arg name}
610    } -nulldefault]
611
612    Twapi_NetLocalGroupMembers 1 $opts(system) $grpname [expr {$opts(type) eq "sid" ? 0 : 3}] $accts
613}
614
615
616# Get rights for an account
617proc twapi::get_account_rights {account args} {
618    array set opts [parseargs args {
619        {system.arg ""}
620    } -maxleftover 0]
621
622    set sid [map_account_to_sid $account -system $opts(system)]
623
624    trap {
625        set lsah [get_lsa_policy_handle -system $opts(system) -access policy_lookup_names]
626        return [Twapi_LsaEnumerateAccountRights $lsah $sid]
627    } onerror {TWAPI_WIN32 2} {
628        # No specific rights for this account
629        return [list ]
630    } finally {
631        if {[info exists lsah]} {
632            close_lsa_policy_handle $lsah
633        }
634    }
635}
636
637# Get accounts having a specific right
638proc twapi::find_accounts_with_right {right args} {
639    array set opts [parseargs args {
640        {system.arg ""}
641        name
642    } -maxleftover 0]
643
644    trap {
645        set lsah [get_lsa_policy_handle \
646                      -system $opts(system) \
647                      -access {
648                          policy_lookup_names
649                          policy_view_local_information
650                      }]
651        set accounts [list ]
652        foreach sid [Twapi_LsaEnumerateAccountsWithUserRight $lsah $right] {
653            if {$opts(name)} {
654                if {[catch {lappend accounts [lookup_account_sid $sid -system $opts(system)]}]} {
655                    # No mapping for SID - can happen if account has been
656                    # deleted but LSA policy not updated accordingly
657                    lappend accounts $sid
658                }
659            } else {
660                lappend accounts $sid
661            }
662        }
663        return $accounts
664    } onerror {TWAPI_WIN32 259} {
665        # No accounts have this right
666        return [list ]
667    } finally {
668        if {[info exists lsah]} {
669            close_lsa_policy_handle $lsah
670        }
671    }
672
673}
674
675# Add/remove rights to an account
676proc twapi::_modify_account_rights {operation account rights args} {
677    set switches {
678        system.arg
679        handle.arg
680    }
681
682    switch -exact -- $operation {
683        add {
684            # Nothing to do
685        }
686        remove {
687            lappend switches all
688        }
689        default {
690            error "Invalid operation '$operation' specified"
691        }
692    }
693
694    array set opts [parseargs args $switches -maxleftover 0]
695
696    if {[info exists opts(system)] && [info exists opts(handle)]} {
697        error "Options -system and -handle may not be specified together"
698    }
699
700    if {[info exists opts(handle)]} {
701        set lsah $opts(handle)
702        set sid $account
703    } else {
704        if {![info exists opts(system)]} {
705            set opts(system) ""
706        }
707
708        set sid [map_account_to_sid $account -system $opts(system)]
709        # We need to open a policy handle ourselves. First try to open
710        # with max privileges in case the account needs to be created
711        # and then retry with lower privileges if that fails
712        catch {
713            set lsah [get_lsa_policy_handle \
714                          -system $opts(system) \
715                          -access {
716                              policy_lookup_names
717                              policy_create_account
718                          }]
719        }
720        if {![info exists lsah]} {
721            set lsah [get_lsa_policy_handle \
722                          -system $opts(system) \
723                          -access policy_lookup_names]
724        }
725    }
726
727    trap {
728        if {$operation == "add"} {
729            LsaAddAccountRights $lsah $sid $rights
730        } else {
731            LsaRemoveAccountRights $lsah $sid $opts(all) $rights
732        }
733    } finally {
734        # Close the handle if we opened it
735        if {! [info exists opts(handle)]} {
736            close_lsa_policy_handle $lsah
737        }
738    }
739}
740
741interp alias {} twapi::add_account_rights {} twapi::_modify_account_rights add
742interp alias {} twapi::remove_account_rights {} twapi::_modify_account_rights remove
743
744# Return list of logon sesionss
745proc twapi::find_logon_sessions {args} {
746    array set opts [parseargs args {
747        user.arg
748        type.arg
749        tssession.arg
750    } -maxleftover 0]
751
752    set luids [LsaEnumerateLogonSessions]
753    if {! ([info exists opts(user)] || [info exists opts(type)] ||
754           [info exists opts(tssession)])} {
755        return $luids
756    }
757
758
759    # Need to get the data for each session to see if it matches
760    set result [list ]
761    if {[info exists opts(user)]} {
762        set sid [map_account_to_sid $opts(user)]
763    }
764    if {[info exists opts(type)]} {
765        set logontypes [list ]
766        foreach logontype $opts(type) {
767            lappend logontypes [_logon_session_type_code $logontype]
768        }
769    }
770
771    foreach luid $luids {
772        trap {
773            unset -nocomplain session
774            array set session [LsaGetLogonSessionData $luid]
775
776            # For the local system account, no data is returned on some
777            # platforms
778            if {[array size session] == 0} {
779                set session(Sid) S-1-5-18; # SYSTEM
780                set session(Session) 0
781                set session(LogonType) 0
782            }
783            if {[info exists opts(user)] && $session(Sid) ne $sid} {
784                continue;               # User id does not match
785            }
786
787            if {[info exists opts(type)] && [lsearch -exact $logontypes $session(LogonType)] < 0} {
788                continue;               # Type does not match
789            }
790
791            if {[info exists opts(tssession)] && $session(Session) != $opts(tssession)} {
792                continue;               # Term server session does not match
793            }
794
795            lappend result $luid
796
797        } onerror {TWAPI_WIN32 1312} {
798            # Session no longer exists. Just skip
799            continue
800        }
801    }
802
803    return $result
804}
805
806
807# Return data for a logon session
808proc twapi::get_logon_session_info {luid args} {
809    array set opts [parseargs args {
810        all
811        authpackage
812        dnsdomain
813        logondomain
814        logonid
815        logonserver
816        logontime
817        type
818        usersid
819        user
820        tssession
821        userprincipal
822    } -maxleftover 0]
823
824    array set session [LsaGetLogonSessionData $luid]
825
826    # Some fields may be missing on Win2K
827    foreach fld {LogonServer DnsDomainName Upn} {
828        if {![info exists session($fld)]} {
829            set session($fld) ""
830        }
831    }
832
833    array set result [list ]
834    foreach {opt index} {
835        authpackage AuthenticationPackage
836        dnsdomain   DnsDomainName
837        logondomain LogonDomain
838        logonid     LogonId
839        logonserver LogonServer
840        logontime   LogonTime
841        type        LogonType
842        usersid         Sid
843        user        UserName
844        tssession   Session
845        userprincipal Upn
846    } {
847        if {$opts(all) || $opts($opt)} {
848            set result(-$opt) $session($index)
849        }
850    }
851
852    if {[info exists result(-type)]} {
853        set result(-type) [_logon_session_type_symbol $result(-type)]
854    }
855
856    return [array get result]
857}
858
859
860
861
862# Set/reset the given bits in the usri3_flags field for a user account
863# mask indicates the mask of bits to set. values indicates the values
864# of those bits
865proc twapi::_change_user_info_flags {username mask values args} {
866    array set opts [parseargs args {
867        system.arg
868    } -nulldefault -maxleftover 0]
869
870    # Get current flags
871    set flags [USER_INFO_1 -flags [NetUserGetInfo $opts(system) $username 1]]
872
873    # Turn off mask bits and write flags back
874    set flags [expr {$flags & (~ $mask)}]
875    # Set the specified bits
876    set flags [expr {$flags | ($values & $mask)}]
877
878    # Write new flags back
879    Twapi_NetUserSetInfo 1008 $opts(system) $username $flags
880}
881
882# Returns the logon session type value for a symbol
883twapi::proc* twapi::_logon_session_type_code {type} {
884    variable _logon_session_type_map
885    # Variable that maps logon session type codes to integer values
886    # Position of each symbol gives its corresponding type value
887    # See ntsecapi.h for definitions
888    set _logon_session_type_map {
889        0
890        1
891        interactive
892        network
893        batch
894        service
895        proxy
896        unlockworkstation
897        networkclear
898        newcredentials
899        remoteinteractive
900        cachedinteractive
901        cachedremoteinteractive
902        cachedunlockworkstation
903    }
904} {
905    variable _logon_session_type_map
906
907    # Type may be an integer or a token
908    set code [lsearch -exact $_logon_session_type_map $type]
909    if {$code >= 0} {
910        return $code
911    }
912
913    if {![string is integer -strict $type]} {
914        badargs! "Invalid logon session type '$type' specified" 3
915    }
916    return $type
917}
918
919# Returns the logon session type symbol for an integer value
920proc twapi::_logon_session_type_symbol {code} {
921    variable _logon_session_type_map
922    _logon_session_type_code interactive; # Just to init _logon_session_type_map
923    set symbol [lindex $_logon_session_type_map $code]
924    if {$symbol eq ""} {
925        return $code
926    } else {
927        return $symbol
928    }
929}
930
931proc twapi::_set_user_priv_level {username priv_level args} {
932
933    array set opts [parseargs args {system.arg} -nulldefault]
934
935    if {0} {
936        # FOr some reason NetUserSetInfo cannot change priv level
937        # Tried it separately with a simple C program. So this code
938        # is commented out and we use group membership to achieve
939        # the desired result
940        # Note: - latest MSDN confirms above
941        if {![info exists twapi::priv_level_map($priv_level)]} {
942            error "Invalid privilege level value '$priv_level' specified. Must be one of [join [array names twapi::priv_level_map] ,]"
943        }
944        set priv $twapi::priv_level_map($priv_level)
945
946        Twapi_NetUserSetInfo_priv $opts(system) $username $priv
947    } else {
948        # Don't hardcode group names - reverse map SID's instead for
949        # non-English systems. Also note that since
950        # we might be lowering privilege level, we have to also
951        # remove from higher privileged groups
952
953        switch -exact -- $priv_level {
954            guest {
955                # administrators users
956                set outgroups {S-1-5-32-544 S-1-5-32-545}
957                # guests
958                set ingroup S-1-5-32-546
959            }
960            user  {
961                # administrators
962                set outgroups {S-1-5-32-544}
963                # users
964                set ingroup S-1-5-32-545
965            }
966            admin {
967                set outgroups {}
968                set ingroup S-1-5-32-544
969            }
970            default {error "Invalid privilege level '$priv_level'. Must be one of 'guest', 'user' or 'admin'"}
971        }
972        # Remove from higher priv groups
973        foreach outgroup $outgroups {
974            # Get the potentially localized name of the group
975            set group [lookup_account_sid $outgroup -system $opts(system)]
976            # Catch since may not be member of that group
977            catch {remove_member_from_local_group $group $username -system $opts(system)}
978        }
979
980        # Get the potentially localized name of the group to be added
981        set group [lookup_account_sid $ingroup -system $opts(system)]
982        add_member_to_local_group $group $username -system $opts(system)
983    }
984}
985
986proc twapi::_map_userinfo_flags {flags} {
987    # UF_LOCKOUT -> 0x10, UF_ACCOUNTDISABLE -> 0x2
988    if {$flags & 0x2} {
989        set status disabled
990    } elseif {$flags & 0x10} {
991        set status locked
992    } else {
993        set status enabled
994    }
995
996    #define UF_TEMP_DUPLICATE_ACCOUNT       0x0100
997    #define UF_NORMAL_ACCOUNT               0x0200
998    #define UF_INTERDOMAIN_TRUST_ACCOUNT    0x0800
999    #define UF_WORKSTATION_TRUST_ACCOUNT    0x1000
1000    #define UF_SERVER_TRUST_ACCOUNT         0x2000
1001    if {$flags & 0x0200} {
1002        set type normal
1003    } elseif {$flags & 0x0100} {
1004        set type duplicate
1005    } elseif {$flags & 0x0800} {
1006        set type interdomain_trust
1007    } elseif {$flags & 0x1000} {
1008        set type workstation_trust
1009    } elseif {$flags & 0x2000} {
1010        set type server_trust
1011    } else {
1012        set type unknown
1013    }
1014
1015    set pw {}
1016    #define UF_PASSWD_NOTREQD                  0x0020
1017    if {$flags & 0x0020} {
1018        lappend pw not_required
1019    }
1020    #define UF_PASSWD_CANT_CHANGE              0x0040
1021    if {$flags & 0x0040} {
1022        lappend pw cannot_change
1023    }
1024    #define UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED 0x0080
1025    if {$flags & 0x0080} {
1026        lappend pw encrypted_text_allowed
1027    }
1028    #define UF_DONT_EXPIRE_PASSWD                         0x10000
1029    if {$flags & 0x10000} {
1030        lappend pw no_expiry
1031    }
1032    #define UF_SMARTCARD_REQUIRED                         0x40000
1033    if {$flags & 0x40000} {
1034        lappend pw smartcard_required
1035    }
1036    #define UF_PASSWORD_EXPIRED                          0x800000
1037    if {$flags & 0x800000} {
1038        lappend pw expired
1039    }
1040
1041    return [list -status $status -type $type -password_attrs $pw]
1042}
1043
1044twapi::proc* twapi::_define_user_modals {} {
1045    struct _USER_MODALS_INFO_0 {
1046        DWORD min_passwd_len;
1047        DWORD max_passwd_age;
1048        DWORD min_passwd_age;
1049        DWORD force_logoff;
1050        DWORD password_hist_len;
1051    }
1052    struct _USER_MODALS_INFO_1 {
1053        DWORD  role;
1054        LPWSTR primary;
1055    }
1056    struct _USER_MODALS_INFO_2 {
1057        LPWSTR domain_name;
1058        PSID   domain_id;
1059    }
1060    struct _USER_MODALS_INFO_3 {
1061        DWORD lockout_duration;
1062        DWORD lockout_observation_window;
1063        DWORD lockout_threshold;
1064    }
1065    struct _USER_MODALS_INFO_1001 {
1066        DWORD min_passwd_len;
1067    }
1068    struct _USER_MODALS_INFO_1002 {
1069        DWORD max_passwd_age;
1070    }
1071    struct _USER_MODALS_INFO_1003 {
1072        DWORD min_passwd_age;
1073    }
1074    struct _USER_MODALS_INFO_1004 {
1075        DWORD force_logoff;
1076    }
1077    struct _USER_MODALS_INFO_1005 {
1078        DWORD password_hist_len;
1079    }
1080    struct _USER_MODALS_INFO_1006 {
1081        DWORD role;
1082    }
1083    struct _USER_MODALS_INFO_1007 {
1084        LPWSTR primary;
1085    }
1086} {
1087}
1088
1089twapi::proc* twapi::get_password_policy {{server_name ""}} {
1090    _define_user_modals
1091} {
1092    set result [NetUserModalsGet $server_name 0 [_USER_MODALS_INFO_0]]
1093    dict with result {
1094        if {$force_logoff == 4294967295 || $force_logoff == -1} {
1095            set force_logoff never
1096        }
1097        if {$max_passwd_age == 4294967295 || $max_passwd_age == -1} {
1098            set max_passwd_age none
1099        }
1100    }
1101    return $result
1102}
1103
1104# TBD - doc & test
1105twapi::proc* twapi::get_system_role {{server_name ""}} {
1106    _define_user_modals
1107} {
1108    set result [NetUserModalsGet $server_name 1 [_USER_MODALS_INFO_1]]
1109    dict set result role [dict* {
1110        0 standalone 1 member 2 backup 3 primary
1111    } [dict get $result role]]
1112    return $result
1113}
1114
1115# TBD - doc & test
1116twapi::proc* twapi::get_system_domain {{server_name ""}} {
1117    _define_user_modals
1118} {
1119    return [NetUserModalsGet $server_name 2 [_USER_MODALS_INFO_2]]
1120}
1121
1122twapi::proc* twapi::get_lockout_policy {{server_name ""}} {
1123    _define_user_modals
1124} {
1125    return [NetUserModalsGet $server_name 3 [_USER_MODALS_INFO_3]]
1126}
1127
1128# TBD - doc & test
1129twapi::proc* twapi::set_password_policy {name val {server_name ""}} {
1130    _define_user_modals
1131} {
1132    switch -exact $name {
1133        min_passwd_len {
1134            NetUserModalsSet $server_name 1001 [_USER_MODALS_INFO_1001 $val]
1135        }
1136        max_passwd_age {
1137            if {$val eq "none"} {
1138                set val 4294967295
1139            }
1140            NetUserModalsSet $server_name 1002 [_USER_MODALS_INFO_1002 $val]
1141        }
1142        min_passwd_age {
1143            NetUserModalsSet $server_name 1003 [_USER_MODALS_INFO_1003 $val]
1144        }
1145        force_logoff {
1146            if {$val eq "never"} {
1147                set val 4294967295
1148            }
1149            NetUserModalsSet $server_name 1004 [_USER_MODALS_INFO_1004 $val]
1150        }
1151        password_hist_len {
1152            NetUserModalsSet $server_name 1005 [_USER_MODALS_INFO_1005 $val]
1153        }
1154    }
1155}
1156
1157# TBD - doc & test
1158twapi::proc* twapi::set_lockout_policy {duration observe_window threshold {server_name ""}} {
1159    _define_user_modals
1160} {
1161    NetUserModalsSet $server_name 3 [_USER_MODALS_INFO_3 $duration $observe_window $threshold]
1162}
1163