Skip to content

Commit 0d6ac87

Browse files
committed
Manually merge PR181 to add refreshusers helper for htaccess sync with associated unit tests
git-svn-id: svn+ssh://svn.code.sf.net/p/migrid/code/trunk@6207 b75ad72c-e7d7-11dd-a971-7dbc132099af
1 parent f9ba9d8 commit 0d6ac87

File tree

3 files changed

+431
-2
lines changed

3 files changed

+431
-2
lines changed

mig/server/refreshusers.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# --- BEGIN_HEADER ---
5+
#
6+
# refreshusers - a simple helper to refresh stale user files to current user ID
7+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
8+
#
9+
# This file is part of MiG.
10+
#
11+
# MiG is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# MiG is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
24+
# USA.
25+
#
26+
# --- END_HEADER ---
27+
#
28+
29+
"""Refresh one or more accounts so that files and dirs fit current user ID, in
30+
particular replace any stale .htaccess files no longer in sync regarding
31+
assigned IDs and therefore causing auth error upon fileman open, etc.
32+
"""
33+
34+
from __future__ import print_function
35+
from __future__ import absolute_import
36+
37+
import datetime
38+
import getopt
39+
import sys
40+
import time
41+
42+
from mig.shared.defaults import gdp_distinguished_field
43+
from mig.shared.useradm import init_user_adm, search_users, default_search, \
44+
assure_current_htaccess
45+
46+
47+
def usage(name='refreshusers.py'):
48+
"""Usage help"""
49+
50+
print("""Refresh MiG user user files and dirs based on user ID in MiG user
51+
database.
52+
53+
Usage:
54+
%(name)s [OPTIONS]
55+
Where OPTIONS may be one or more of:
56+
-A EXPIRE_AFTER Limit to users expiring after EXPIRE_AFTER (epoch)
57+
-B EXPIRE_BEFORE Limit to users expiring before EXPIRE_BEFORE (epoch)
58+
-c CONF_FILE Use CONF_FILE as server configuration
59+
-d DB_FILE Use DB_FILE as user data base file
60+
-f Force operations to continue past errors
61+
-h Show this help
62+
-I CERT_DN Filter to user(s) with ID (distinguished name)
63+
-s SHORT_ID Filter to user(s) with given short ID field
64+
-v Verbose output
65+
"""
66+
% {'name': name})
67+
68+
69+
if '__main__' == __name__:
70+
(args, app_dir, db_path) = init_user_adm()
71+
conf_path = None
72+
force = False
73+
verbose = False
74+
exit_code = 0
75+
now = int(time.time())
76+
search_filter = default_search()
77+
# Default to all users with expire range between now and in 30 days
78+
search_filter['distinguished_name'] = '*'
79+
search_filter['short_id'] = '*'
80+
search_filter['expire_after'] = now
81+
search_filter['expire_before'] = int(time.time() + 365 * 24 * 3600)
82+
# Default to only external openid accounts
83+
services = ['extoid']
84+
opt_args = 'A:B:c:d:fhI:s:v'
85+
try:
86+
(opts, args) = getopt.getopt(args, opt_args)
87+
except getopt.GetoptError as err:
88+
print('Error: ', err.msg)
89+
usage()
90+
sys.exit(1)
91+
92+
for (opt, val) in opts:
93+
if opt == '-A':
94+
after = now
95+
if val.startswith('+'):
96+
after += int(val[1:])
97+
elif val.startswith('-'):
98+
after -= int(val[1:])
99+
else:
100+
after = int(val)
101+
search_filter['expire_after'] = after
102+
elif opt == '-B':
103+
before = now
104+
if val.startswith('+'):
105+
before += int(val[1:])
106+
elif val.startswith('-'):
107+
before -= int(val[1:])
108+
else:
109+
before = int(val)
110+
search_filter['expire_before'] = before
111+
elif opt == '-c':
112+
conf_path = val
113+
elif opt == '-d':
114+
db_path = val
115+
elif opt == '-f':
116+
force = True
117+
elif opt == '-h':
118+
usage()
119+
sys.exit(0)
120+
elif opt == '-I':
121+
search_filter['distinguished_name'] = val
122+
elif opt == '-s':
123+
search_filter['short_id'] = val
124+
elif opt == '-v':
125+
verbose = True
126+
else:
127+
print('Error: %s not supported!' % opt)
128+
sys.exit(1)
129+
130+
if args:
131+
print('Error: Non-option arguments are not supported - missing quotes?')
132+
usage()
133+
sys.exit(1)
134+
135+
(configuration, hits) = search_users(search_filter, conf_path, db_path,
136+
verbose)
137+
logger = configuration.logger
138+
gdp_prefix = "%s=" % gdp_distinguished_field
139+
# NOTE: we already filtered expired accounts here
140+
search_dn = search_filter['distinguished_name']
141+
before = datetime.datetime.fromtimestamp(search_filter['expire_before'])
142+
after = datetime.datetime.fromtimestamp(search_filter['expire_after'])
143+
if verbose:
144+
if hits:
145+
print("Check %d account(s) expiring between %s and %s for ID %r" %
146+
(len(hits), after, before, search_dn))
147+
else:
148+
print("No accounts expire between %s and %s for ID %r" %
149+
(after, before, search_dn))
150+
151+
for (user_id, user_dict) in hits:
152+
affected = []
153+
if verbose:
154+
print('Check refresh needed for %r' % user_id)
155+
156+
# NOTE: gdp accounts don't actually use .htaccess but cat.py serving
157+
if configuration.site_enable_gdp and \
158+
user_id.split('/')[-1].startswith(gdp_prefix):
159+
if verbose:
160+
print("Handling GDP project account %r despite no effect" %
161+
user_id)
162+
163+
# Don't warn about already disabled or suspended accounts
164+
account_state = user_dict.get('status', 'active')
165+
if not account_state in ('active', 'temporal'):
166+
if verbose:
167+
print('Skip handling of already %s user %r' % (account_state,
168+
user_id))
169+
continue
170+
171+
known_auth = user_dict.get('auth', [])
172+
if not known_auth:
173+
if user_dict.get('main_id', ''):
174+
known_auth.append("extoidc")
175+
elif user_dict.get('openid_names', []):
176+
if user_dict.get('password_hash', ''):
177+
known_auth.append("migoid")
178+
else:
179+
known_auth.append("extoid")
180+
elif user_dict.get('password', ''):
181+
known_auth.append("migcert")
182+
else:
183+
if verbose:
184+
print('Skip handling of user %r without auth info' %
185+
user_id)
186+
continue
187+
188+
if not ('extoid' in known_auth or 'extoidc' in known_auth):
189+
if verbose:
190+
print('Skip handling of user %r without extoid(c) auth' %
191+
user_id)
192+
continue
193+
194+
if verbose:
195+
print('Assure current htaccess for %r account' % user_id)
196+
if not assure_current_htaccess(configuration, user_id, user_dict,
197+
force, verbose):
198+
exit_code += 1
199+
200+
sys.exit(exit_code)

mig/shared/useradm.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --- BEGIN_HEADER ---
55
#
66
# useradm - user administration functions
7-
# Copyright (C) 2003-2024 The MiG Project lead by Brian Vinter
7+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
88
#
99
# This file is part of MiG.
1010
#
@@ -22,7 +22,7 @@
2222
# along with this program; if not, write to the Free Software
2323
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
2424
#
25-
# -- END_HEADER ---
25+
# --- END_HEADER ---
2626
#
2727

2828
"""User administration functions"""
@@ -1916,6 +1916,59 @@ def expire_oid_sessions(configuration, db_name, identity):
19161916
return __oid_sessions_execute(configuration, db_name, query, args, True)
19171917

19181918

1919+
def assure_current_htaccess(configuration, client_id, user_dict, force=False,
1920+
verbose=False):
1921+
"""Check and force htaccess file renew for client_id to refresh any stale
1922+
old ID data no longer in sync with user DB.
1923+
"""
1924+
logger = configuration.logger
1925+
now = time.time()
1926+
client_dir = client_id_dir(client_id)
1927+
user_home = os.path.join(configuration.user_home, client_dir)
1928+
user_cache = os.path.join(configuration.user_cache, client_dir)
1929+
try:
1930+
htaccess_path = os.path.join(user_home, htaccess_filename)
1931+
if not os.path.isfile(htaccess_path):
1932+
htaccess_contents = ''
1933+
else:
1934+
htaccess_contents = read_file(htaccess_path, logger)
1935+
# The .htaccess file needs to have auth lines on the format
1936+
# require user "abc123@somewhere.org"
1937+
# for each abc123@somewhere.org from the openid_names of the account
1938+
# entry in the user database.
1939+
check_required_names = user_dict.get('openid_names', [])
1940+
short_id = user_dict.get('short_id', '')
1941+
if short_id and short_id not in check_required_names:
1942+
check_required_names.append(short_id)
1943+
missing_names = []
1944+
for username in check_required_names:
1945+
required_line = 'require user "%s"' % username
1946+
if htaccess_contents.find(required_line) == -1:
1947+
missing_names.append(username)
1948+
if not missing_names:
1949+
if verbose:
1950+
print('Account %r htaccess already up to date' % client_id)
1951+
return True
1952+
if verbose:
1953+
print('Account %r requires htaccess refresh: %s missing' %
1954+
(client_id, ', '.join(missing_names)))
1955+
# NOTE: backup current htaccess in user_cache as safety
1956+
htaccess_backup = os.path.join(user_cache, htaccess_filename + '.old')
1957+
if verbose:
1958+
print('write htaccess backup in %s' % htaccess_backup)
1959+
write_file(htaccess_contents, htaccess_backup, logger)
1960+
create_user_in_fs(configuration, client_id, user_dict, now, True,
1961+
force, verbose)
1962+
if verbose:
1963+
print('Refreshed %r in file system' % client_id)
1964+
return True
1965+
except Exception as exc:
1966+
if not force:
1967+
raise Exception('Could not refresh %r in file system: %s' %
1968+
(client_id, exc))
1969+
return False
1970+
1971+
19191972
def migrate_users(
19201973
conf_path,
19211974
db_path,

0 commit comments

Comments
 (0)