001package jmri.jmrit.permission;
002
003import java.awt.GraphicsEnvironment;
004import java.security.MessageDigest;
005import java.security.NoSuchAlgorithmException;
006import java.util.*;
007
008import javax.xml.bind.DatatypeConverter;
009
010import jmri.*;
011import jmri.PermissionValue;
012import jmri.util.swing.JmriJOptionPane;
013
014/**
015 * The default implementation of User.
016 *
017 * @author Daniel Bergqvist (C) 2024
018 */
019@edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="DMI_RANDOM_USED_ONLY_ONCE",
020    justification = "False positive. The Random instance is kept by the iterator.")
021
022public class DefaultUser implements User {
023
024    private final String _username;
025    private final String _systemUserName;
026    private final boolean _systemUser;
027    private final int _priority;
028    private String _seed;
029    private String _passwordMD5;
030    private String _name = "";
031    private String _comment = "";
032
033    private final Set<Role> _roles = new TreeSet<>((a,b) -> {return a.getName().compareTo(b.getName());});
034
035    public DefaultUser(String username, String password) {
036        this(username, password, 0, null, new Role[]{});
037        DefaultUser.this.addRole(DefaultRole.ROLE_STANDARD_USER);
038    }
039
040    DefaultUser(DefaultUser u) {
041        _username = u._username;
042        _systemUserName = u._systemUserName;
043        _systemUser = u._systemUser;
044        _priority = u._priority;
045        _seed = u._seed;
046        _passwordMD5 = u._passwordMD5;
047        _name = u._name;
048        _comment = u._comment;
049        _roles.addAll(u._roles);
050    }
051
052    DefaultUser(String username, String password, int priority, String systemUserName, Role[] roles) {
053        this._username = username;
054        this._priority = priority;
055        this._systemUser = priority != 0;
056        this._systemUserName = systemUserName;
057        if (password != null) {
058            this._seed = getRandomString(10);
059            try {
060                this._passwordMD5 = getPasswordSHA256(password);
061            } catch (NoSuchAlgorithmException e) {
062                log.error("MD5 algoritm doesn't exists", e);
063            }
064        } else {
065            this._seed = null;
066        }
067        for (Role role : roles) {
068            _roles.add(role);
069        }
070    }
071
072    public DefaultUser(String username, String passwordMD5, String seed) {
073        this._username = username;
074        this._systemUserName = null;
075        this._systemUser = false;
076        this._priority = 0;
077        this._passwordMD5 = passwordMD5;
078        this._seed = seed;
079    }
080
081    private static final PrimitiveIterator.OfInt iterator =
082            new Random().ints('a', 'z'+10).iterator();
083
084    public static String getRandomString(int count) {
085        StringBuilder s = new StringBuilder();
086        for (int i=0; i < count; i++) {
087            int r = iterator.nextInt();
088            char c = (char) (r > 'z' ? r-'z'+'0' : r);
089            s.append(c);
090        }
091        return s.toString();
092    }
093
094    @Override
095    public String getUserName() {
096        return this._username;
097    }
098
099    @Override
100    public boolean isSystemUser() {
101        return this._systemUser;
102    }
103
104    @Override
105    public int getPriority() {
106        return this._priority;
107    }
108
109    String getSystemUsername() {
110        return this._systemUserName;
111    }
112
113    String getPassword() {
114        return this._passwordMD5;
115    }
116
117    void setPasswordMD5(String passwordMD5) {
118        this._passwordMD5 = passwordMD5;
119    }
120
121    String getSeed() {
122        return this._seed;
123    }
124
125    void setSeed(String seed) {
126        this._seed = seed;
127    }
128
129    @Override
130    public String getName() {
131        return _name;
132    }
133
134    @Override
135    public void setName(String name) {
136        this._name = name;
137    }
138
139    @Override
140    public String getComment() {
141        return _comment;
142    }
143
144    @Override
145    public void setComment(String comment) {
146        this._comment = comment;
147    }
148
149    @Override
150    public Set<Role> getRoles() {
151        return Collections.unmodifiableSet(_roles);
152    }
153
154    @Override
155    public void addRole(Role role) {
156        if (! InstanceManager.getDefault(PermissionManager.class)
157                .ensureAtLeastPermission(PermissionsSystemAdmin.PERMISSION_EDIT_PREFERENCES,
158                        BooleanPermission.BooleanValue.TRUE)) {
159            return;
160        }
161        _roles.add(role);
162    }
163
164    @Override
165    public void removeRole(Role role) {
166        if (! InstanceManager.getDefault(PermissionManager.class)
167                .ensureAtLeastPermission(PermissionsSystemAdmin.PERMISSION_EDIT_PREFERENCES,
168                        BooleanPermission.BooleanValue.TRUE)) {
169            return;
170        }
171        _roles.remove(role);
172    }
173
174    void setRoles(Set<Role> roles) {
175        _roles.clear();
176        _roles.addAll(roles);
177    }
178
179    private String getPasswordSHA256(String password) throws NoSuchAlgorithmException {
180        String passwd = this._seed + password;
181        MessageDigest md = MessageDigest.getInstance("SHA-256");
182        md.update(passwd.getBytes());
183        return DatatypeConverter
184                .printHexBinary(md.digest()).toUpperCase();
185    }
186
187    @Override
188    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
189        justification="The text is from an exception")
190    public void setPassword(String newPassword) {
191        PermissionManager pMngr = InstanceManager.getDefault(PermissionManager.class);
192
193        if (!pMngr.hasAtLeastPermission(
194                PermissionsSystemAdmin.PERMISSION_EDIT_PERMISSIONS,
195                BooleanPermission.BooleanValue.TRUE)) {
196            log.warn("The current user has not permission to change password for user {}", getUserName());
197
198            if (!GraphicsEnvironment.isHeadless()) {
199                JmriJOptionPane.showMessageDialog(null,
200                        Bundle.getMessage("DefaultPermissionManager_PermissionDenied"),
201                        jmri.Application.getApplicationName(),
202                        JmriJOptionPane.ERROR_MESSAGE);
203            }
204        }
205
206        try {
207            this._passwordMD5 = getPasswordSHA256(newPassword);
208        } catch (NoSuchAlgorithmException e) {
209            String msg = "MD5 algoritm doesn't exists";
210            log.error(msg);
211            throw new RuntimeException(msg);
212        }
213    }
214
215    @Override
216    public boolean isPermittedToChangePassword() {
217        PermissionManager pMngr = InstanceManager.getDefault(PermissionManager.class);
218
219        boolean isCurrentUser = pMngr.isCurrentUser(this);
220        boolean hasEditPasswordPermission = pMngr.hasAtLeastPermission(
221                PermissionsSystemAdmin.PERMISSION_EDIT_OWN_PASSWORD,
222                BooleanPermission.BooleanValue.TRUE);
223        boolean hasAdminPermission = pMngr.hasAtLeastPermission(
224                PermissionsSystemAdmin.PERMISSION_EDIT_PERMISSIONS,
225                BooleanPermission.BooleanValue.TRUE);
226
227        return (hasAdminPermission || (isCurrentUser && hasEditPasswordPermission));
228    }
229
230    @Override
231    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
232        justification="The text is from an exception")
233    public boolean changePassword(String oldPassword, String newPassword) {
234        PermissionManager pMngr = InstanceManager.getDefault(PermissionManager.class);
235
236        if (isPermittedToChangePassword()) {
237            if (!checkPassword(oldPassword)) {
238                String msg = new PermissionManager.BadPasswordException().getMessage();
239
240                if (!GraphicsEnvironment.isHeadless()) {
241                    JmriJOptionPane.showMessageDialog(null,
242                            msg,
243                            jmri.Application.getApplicationName(),
244                            JmriJOptionPane.ERROR_MESSAGE);
245                } else {
246                    log.error(msg);
247                }
248            } else {
249                try {
250                    this._passwordMD5 = getPasswordSHA256(newPassword);
251                    return true;
252                } catch (NoSuchAlgorithmException e) {
253                    String msg = "MD5 algoritm doesn't exists";
254                    log.error(msg);
255                    throw new RuntimeException(msg);
256                }
257            }
258        } else {
259            if (pMngr.isCurrentUser(this)) {
260                log.warn("User {} has not permission to change its own password", getUserName());
261            } else {
262                log.warn("The current user has not permission to change password for user {}", getUserName());
263            }
264            if (!GraphicsEnvironment.isHeadless()) {
265                JmriJOptionPane.showMessageDialog(null,
266                        Bundle.getMessage("DefaultPermissionManager_PermissionDenied"),
267                        jmri.Application.getApplicationName(),
268                        JmriJOptionPane.ERROR_MESSAGE);
269            }
270        }
271        return false;
272    }
273
274    public boolean checkPassword(String password) {
275        try {
276            return _passwordMD5.equals(getPasswordSHA256(password));
277        } catch (NoSuchAlgorithmException e) {
278            String msg = "MD5 algoritm doesn't exists";
279            log.error(msg);
280            throw new RuntimeException(msg);
281        }
282    }
283
284    @Override
285    public boolean hasAtLeastPermission(Permission permission, PermissionValue minValue) {
286        PermissionValue lastValue = null;
287        // Try to find the highest permission this user has
288        for (Role role : _roles) {
289            PermissionValue value = role.getPermissionValue(permission);
290            if (lastValue == null || permission.compare(lastValue, value) < 0) {
291                lastValue = value;
292            }
293        }
294        if (lastValue == null || lastValue.isDefault()) {
295            // No role of this user have a permission value set, or the
296            // permission value is the default.
297            // Try to find the highest default permission this user has
298            for (Role role : _roles) {
299                PermissionValue value = permission.getDefaultPermission(role);
300                if (lastValue == null || permission.compare(lastValue, value) < 0) {
301                    lastValue = value;
302                }
303            }
304        }
305        if (lastValue == null || lastValue.isDefault()) {
306            // This user has no roles or the permission value is the default.
307            // Get the default permission if no role.
308            lastValue = permission.getDefaultPermission();
309        }
310        return permission.compare(minValue, lastValue) <= 0;
311    }
312
313    @Override
314    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
315        justification="The text is from a bundle")
316    public boolean ensureAtLeastPermission(Permission permission, PermissionValue minValue) {
317        if (!hasAtLeastPermission(permission, minValue)) {
318            log.warn("User {} has not permission {}", this.getUserName(), permission.getName());
319            if (!GraphicsEnvironment.isHeadless()) {
320                JmriJOptionPane.showMessageDialog(null,
321                        Bundle.getMessage("DefaultPermissionManager_PermissionDenied"),
322                        jmri.Application.getApplicationName(),
323                        JmriJOptionPane.ERROR_MESSAGE);
324            }
325            return false;
326        }
327        return true;
328    }
329
330    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultUser.class);
331}