|
23 | 23 | 'SearchAction', |
24 | 24 | 'EditCSVAction', 'EditItemAction', 'PassResetAction', |
25 | 25 | 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction', |
26 | | - 'NewItemAction', 'ExportCSVAction', 'ExportCSVWithIdAction'] |
| 26 | + 'NewItemAction', 'ExportCSVAction', 'ExportCSVWithIdAction', |
| 27 | + 'ReauthAction'] |
27 | 28 |
|
28 | 29 |
|
29 | 30 | class Action: |
@@ -1843,6 +1844,134 @@ def handle(self): |
1843 | 1844 |
|
1844 | 1845 | return '\n' |
1845 | 1846 |
|
| 1847 | +class ReauthAction(Action): |
| 1848 | + '''Allow an auditor to require change verification with user's password. |
| 1849 | +
|
| 1850 | + When changing sensitive information (e.g. passwords) it is |
| 1851 | + useful to ask for a validated authorization. This makes sure |
| 1852 | + that the user is present by typing their password. |
| 1853 | +
|
| 1854 | + In an auditor adding:: |
| 1855 | +
|
| 1856 | + if 'password' in newvalues and not getattr(db, 'reauth_done', False): |
| 1857 | + raise Reauth() |
| 1858 | +
|
| 1859 | + will present the user with a authorization page when the |
| 1860 | + password is changed. The page is generated from the |
| 1861 | + _generic.reauth.html template by default. |
| 1862 | +
|
| 1863 | + Once the user enters their password and submits the page, the |
| 1864 | + password will be verified using: |
| 1865 | + roundup.cgi.actions:LoginAction::verifyPassword(). If the |
| 1866 | + password is correct the original change is done. |
| 1867 | +
|
| 1868 | + To prevent the auditor from trigering another Reauth, the |
| 1869 | + attribute "reauth_done" is added to the db object. As a result, |
| 1870 | + the getattr call will return True and not raise Reauth. |
| 1871 | +
|
| 1872 | + You get one reauth for the submitted change. Note you cannot |
| 1873 | + Reauth multiple properties separately. If you need to auth |
| 1874 | + multiple properties separately, you need to reject the change |
| 1875 | + and force the user to submit each sensitive property separately. |
| 1876 | + For example:: |
| 1877 | +
|
| 1878 | + if 'password' in newvalues and 'realname' in newvalues: |
| 1879 | + raise Reject('Changing the username and the realname ' |
| 1880 | + 'at the same time is not allowed. Please ' |
| 1881 | + 'submit two changes.') |
| 1882 | +
|
| 1883 | + if 'password' in newvalues and not getattr(db, 'reauth_done', False): |
| 1884 | + raise Reauth() |
| 1885 | +
|
| 1886 | + if 'realname' in newvalues and not getattr(db, 'reauth_done', False): |
| 1887 | + raise Reauth() |
| 1888 | +
|
| 1889 | + Limitations: It can not handle file input fields. |
| 1890 | +
|
| 1891 | + See also: client.py:Client:reauth() which can be changed |
| 1892 | + using interfaces.py in your tracker. |
| 1893 | + ''' |
| 1894 | + |
| 1895 | + def handle(self): |
| 1896 | + ''' Handle a form with a reauth request. |
| 1897 | + ''' |
| 1898 | + |
| 1899 | + if self.client.env['REQUEST_METHOD'] != 'POST': |
| 1900 | + raise Reject(self._('Invalid request')) |
| 1901 | + |
| 1902 | + if '@reauth_password' not in self.form: |
| 1903 | + self.client.add_error_message(self._('Password incorrect.')) |
| 1904 | + return |
| 1905 | + |
| 1906 | + # Verify password |
| 1907 | + login = self.client.get_action_class('login')(self.client) |
| 1908 | + if not self.verifyPassword(): |
| 1909 | + self.client.add_error_message(self._("Password incorrect.")) |
| 1910 | + self.client.template = "reauth" |
| 1911 | + |
| 1912 | + # Strip fields that are added by the template. All |
| 1913 | + # the rest of the fields in self.form.list are added |
| 1914 | + # as hidden fields by the reauth template. |
| 1915 | + self.form.list = [ x for x in self.form.list |
| 1916 | + if x.name not in ( |
| 1917 | + '@action', |
| 1918 | + '@csrf', |
| 1919 | + '@reauth_password', |
| 1920 | + '@template', |
| 1921 | + 'submit' |
| 1922 | + ) |
| 1923 | + ] |
| 1924 | + return |
| 1925 | + |
| 1926 | + # extract the info we need to preserve/reinject into |
| 1927 | + # the next action. |
| 1928 | + |
| 1929 | + if '@next_action' not in self.form: # required to look up next action |
| 1930 | + self.client.add_error_message( |
| 1931 | + self._('Missing action to be authorized.')) |
| 1932 | + return |
| 1933 | + |
| 1934 | + action = self.form['@next_action'].value.lower() |
| 1935 | + |
| 1936 | + next_template = None; # optional |
| 1937 | + if '@next_template' in self.form: |
| 1938 | + next_template = self.form['@next_template'].value |
| 1939 | + |
| 1940 | + # rewrite the form for redisplay |
| 1941 | + # remove all the reauth_form_fields leaving just the original |
| 1942 | + # form fields from the form that trigered the reauth. |
| 1943 | + # We extracted @next_* above to route to the original action |
| 1944 | + # and template. |
| 1945 | + |
| 1946 | + reauth_form_fields = ('@reauth_password', '@template', |
| 1947 | + '@next_template', '@action', |
| 1948 | + '@next_action', '@csrf', 'submit') |
| 1949 | + |
| 1950 | + self.form.list = [ x for x in self.form.list |
| 1951 | + if x.name not in reauth_form_fields |
| 1952 | + ] |
| 1953 | + |
| 1954 | + try: |
| 1955 | + action_klass = self.client.get_action_class(action) |
| 1956 | + |
| 1957 | + # set the template to go back to |
| 1958 | + # use "" not None as this value gets encoded and None is invalid |
| 1959 | + self.client.template = next_template if next_template else "" |
| 1960 | + # use this in detector (to skip reauth |
| 1961 | + self.client.db.reauth_done = True |
| 1962 | + |
| 1963 | + # should raise exception Redirect |
| 1964 | + action_klass(self.client).execute() |
| 1965 | + |
| 1966 | + except (ValueError, Reject) as err: |
| 1967 | + escape = not isinstance(err, RejectRaw) |
| 1968 | + self.add_error_message(str(err), escape=escape) |
| 1969 | + |
| 1970 | + def verifyPassword(self): |
| 1971 | + login = self.client.get_action_class('login')(self.client) |
| 1972 | + return login.verifyPassword(self.userid, |
| 1973 | + self.form['@reauth_password'].value) |
| 1974 | + |
1846 | 1975 |
|
1847 | 1976 | class Bridge(BaseAction): |
1848 | 1977 | """Make roundup.actions.Action executable via CGI request. |
|
0 commit comments