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