All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wildfly.security.auth.realm.LegacyPropertiesSecurityRealm Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 34.0.0.Final
Show newest version
/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wildfly.security.auth.realm;

import static org.wildfly.security.auth.realm.ElytronMessages.log;
import static org.wildfly.security.password.interfaces.ClearPassword.ALGORITHM_CLEAR;
import static org.wildfly.security.password.interfaces.DigestPassword.ALGORITHM_DIGEST_MD5;
import static org.wildfly.security.provider.util.ProviderUtil.INSTALLED_PROVIDERS;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.Provider;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import org.wildfly.common.Assert;
import org.wildfly.common.codec.DecodeException;
import org.wildfly.common.iteration.ByteIterator;
import org.wildfly.common.iteration.CodePointIterator;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityRealm;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.authz.AuthorizationIdentity;
import org.wildfly.security.authz.MapAttributes;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.credential.PasswordCredential;
import org.wildfly.security.evidence.Evidence;
import org.wildfly.security.evidence.PasswordGuessEvidence;
import org.wildfly.security.password.Password;
import org.wildfly.security.password.PasswordFactory;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.password.spec.DigestPasswordAlgorithmSpec;
import org.wildfly.security.password.spec.DigestPasswordSpec;
import org.wildfly.security.password.spec.Encoding;
import org.wildfly.security.password.spec.EncryptablePasswordSpec;
import org.wildfly.security.password.spec.PasswordSpec;

/**
 * A {@link SecurityRealm} implementation that makes use of the legacy properties files.
 *
 * @author Darran Lofthouse
 */
public class LegacyPropertiesSecurityRealm implements SecurityRealm {

    private static final String COMMENT_PREFIX1 = "#";
    private static final String COMMENT_PREFIX2 = "!";
    private static final String REALM_COMMENT_PREFIX = "$REALM_NAME=";
    private static final String REALM_COMMENT_SUFFIX = "$";

    private final Supplier providers;
    private final String defaultRealm;
    private final boolean plainText;
    private final Encoding hashEncoding;
    private final Charset hashCharset;

    private final String groupsAttribute;

    private final AtomicReference loadedState = new AtomicReference<>();

    private LegacyPropertiesSecurityRealm(Builder builder) throws IOException {
        plainText = builder.plainText;
        groupsAttribute = builder.groupsAttribute;
        providers = builder.providers;
        defaultRealm = builder.defaultRealm;
        hashEncoding = builder.hashEncoding;
        hashCharset = builder.hashCharset;
    }

    @Override
    public RealmIdentity getRealmIdentity(final Principal principal) throws RealmUnavailableException {
        if (! (principal instanceof NamePrincipal)) {
            log.tracef("PropertiesRealm: unsupported principal type: [%s]", principal);
            return RealmIdentity.NON_EXISTENT;
        }
        final LoadedState loadedState = this.loadedState.get();

        final AccountEntry accountEntry = loadedState.getAccounts().get(principal.getName());

        if (accountEntry == null) {
            log.tracef("PropertiesRealm: identity [%s] does not exist", principal);
            return RealmIdentity.NON_EXISTENT;
        }

        return new RealmIdentity() {

            public Principal getRealmIdentityPrincipal() {
                return principal;
            }

            @Override
            public SupportLevel getCredentialAcquireSupport(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
                return LegacyPropertiesSecurityRealm.this.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec);
            }

            @Override
            public SupportLevel getEvidenceVerifySupport(final Class evidenceType, final String algorithmName) throws RealmUnavailableException {
                return LegacyPropertiesSecurityRealm.this.getEvidenceVerifySupport(evidenceType, algorithmName);
            }

            @Override
            public  C getCredential(final Class credentialType) throws RealmUnavailableException {
                return getCredential(credentialType, null, null);
            }

            @Override
            public  C getCredential(final Class credentialType, final String algorithmName) throws RealmUnavailableException {
                return getCredential(credentialType, algorithmName, null);
            }

            @Override
            public  C getCredential(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
                if (accountEntry.getPasswordRepresentation() == null || LegacyPropertiesSecurityRealm.this.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec) == SupportLevel.UNSUPPORTED) {
                    log.tracef("PropertiesRealm: Unable to obtain credential for identity [%s]", principal);
                    return null;
                }

                boolean clear; // whether should be clear or digested credential returned
                if (algorithmName == null) {
                    clear = plainText;
                } else if (ALGORITHM_CLEAR.equals(algorithmName)) {
                    clear = true;
                } else if (ALGORITHM_DIGEST_MD5.equals(algorithmName)) {
                    clear = false;
                } else {
                    log.tracef("PropertiesRealm: Unable to obtain credential for identity [%s]: unsupported algorithm [%s]", principal, algorithmName);
                    return null;
                }

                final PasswordFactory passwordFactory;
                final PasswordSpec passwordSpec;

                if (clear) {
                    passwordFactory = getPasswordFactory(ALGORITHM_CLEAR);
                    passwordSpec = new ClearPasswordSpec(accountEntry.getPasswordRepresentation().toCharArray());
                } else {
                    passwordFactory = getPasswordFactory(ALGORITHM_DIGEST_MD5);
                    if (plainText) { // file contains clear passwords - needs to be digested
                        AlgorithmParameterSpec spec = parameterSpec != null ? parameterSpec : new DigestPasswordAlgorithmSpec(accountEntry.getName(), loadedState.getRealmName());
                        passwordSpec = new EncryptablePasswordSpec(accountEntry.getPasswordRepresentation().toCharArray(), spec);
                    } else { // already digested file - need to check realm name
                        if (parameterSpec != null) { // when not null, type already checked in acquire support check
                            DigestPasswordAlgorithmSpec spec = (DigestPasswordAlgorithmSpec) parameterSpec;
                            if (! loadedState.getRealmName().equals(spec.getRealm()) || ! accountEntry.getName().equals(spec.getUsername())) {
                                if (log.isTraceEnabled()) {
                                    log.tracef("PropertiesRealm: Unable to obtain credential for username [%s] (available [%s]) and realm [%s] (available [%s])",
                                            spec.getUsername(), accountEntry.getName(), spec.getRealm(), loadedState.getRealmName());
                                }
                                return null; // no digest for given username+realm
                            }
                        }
                        byte[] hashed;
                        if (hashEncoding.equals(Encoding.BASE64)) {
                            hashed = ByteIterator.ofBytes(accountEntry.getPasswordRepresentation().getBytes(hashCharset)).asUtf8String().base64Decode().drain();
                        } else {
                            // use hex by default otherwise
                            hashed = ByteIterator.ofBytes(accountEntry.getPasswordRepresentation().getBytes(hashCharset)).asUtf8String().hexDecode().drain();
                        }
                        passwordSpec = new DigestPasswordSpec(accountEntry.getName(), loadedState.getRealmName(), hashed);
                    }
                }

                try {
                    return credentialType.cast(new PasswordCredential(passwordFactory.generatePassword(passwordSpec)));
                } catch (InvalidKeySpecException e) {
                    throw new IllegalStateException(e);
                }
            }

            @Override
            public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableException {
                if (accountEntry.getPasswordRepresentation() == null || !(evidence instanceof PasswordGuessEvidence)) {
                    log.tracef("Unable to verify evidence for identity [%s]", principal);
                    return false;
                }
                final char[] guess = ((PasswordGuessEvidence) evidence).getGuess();

                final PasswordFactory passwordFactory;
                final PasswordSpec passwordSpec;
                final Password actualPassword;
                if (plainText) {
                    passwordFactory = getPasswordFactory(ALGORITHM_CLEAR);
                    passwordSpec = new ClearPasswordSpec(accountEntry.getPasswordRepresentation().toCharArray());
                } else {
                    passwordFactory = getPasswordFactory(ALGORITHM_DIGEST_MD5);
                    try {
                        byte[] hashed;
                        if (hashEncoding.equals(Encoding.BASE64)) {
                            hashed = ByteIterator.ofBytes(accountEntry.getPasswordRepresentation().getBytes(hashCharset)).asUtf8String().base64Decode().drain();
                        }  else {
                            // use hex by default otherwise
                            hashed = ByteIterator.ofBytes(accountEntry.getPasswordRepresentation().getBytes(hashCharset)).asUtf8String().hexDecode().drain();
                        }
                        passwordSpec = new DigestPasswordSpec(accountEntry.getName(), loadedState.getRealmName(), hashed);
                    } catch (DecodeException e) {
                        throw log.decodingHashedPasswordFromPropertiesRealmFailed(e);
                    }
                }
                try {

                    log.tracef("Attempting to authenticate account %s using LegacyPropertiesSecurityRealm.",
                        accountEntry.getName());

                    actualPassword = passwordFactory.generatePassword(passwordSpec);
                    return passwordFactory.verify(actualPassword, guess, hashCharset);
                } catch (InvalidKeySpecException | InvalidKeyException | IllegalStateException e) {
                    throw new IllegalStateException(e);
                }
            }

            public boolean exists() throws RealmUnavailableException {
                return true;
            }

            @Override
            public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException {
                return AuthorizationIdentity.basicIdentity(new MapAttributes(Collections.singletonMap(groupsAttribute, accountEntry.getGroups())));
            }
        };
    }

    private PasswordFactory getPasswordFactory(final String algorithm) {
        try {
            return PasswordFactory.getInstance(algorithm, providers);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public SupportLevel getCredentialAcquireSupport(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
        Assert.checkNotNullParam("credentialType", credentialType);
        return PasswordCredential.class.isAssignableFrom(credentialType) &&
                (algorithmName == null || algorithmName.equals(ALGORITHM_CLEAR) && plainText || algorithmName.equals(ALGORITHM_DIGEST_MD5)) &&
                (parameterSpec == null || parameterSpec instanceof DigestPasswordAlgorithmSpec)
                ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    @Override
    public SupportLevel getEvidenceVerifySupport(final Class evidenceType, final String algorithmName) throws RealmUnavailableException {
        return PasswordGuessEvidence.class.isAssignableFrom(evidenceType) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    /**
     * Loads this properties security realm from the given user and groups input streams.
     *
     * @param usersStream the input stream from which the realm users are loaded
     * @param groupsStream the input stream from which the roles of realm users are loaded
     * @throws IOException if there is problem while reading the input streams or invalid content is loaded from streams
     */
    public void load(InputStream usersStream, InputStream groupsStream) throws IOException {
        Map accounts = new HashMap<>();
        Properties groups = new Properties();

        if (groupsStream != null) {
            try (InputStreamReader is = new InputStreamReader(groupsStream, StandardCharsets.UTF_8);) {
                groups.load(is);
            }
        }

        String realmName = null;
        if (usersStream != null) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(usersStream, StandardCharsets.UTF_8))) {
                String currentLine;
                while ((currentLine = reader.readLine()) != null) {
                    final String trimmed = currentLine.trim();
                    if (trimmed.startsWith(COMMENT_PREFIX1) && trimmed.contains(REALM_COMMENT_PREFIX)) {
                        // this is the line that contains the realm name.
                        int start = trimmed.indexOf(REALM_COMMENT_PREFIX) + REALM_COMMENT_PREFIX.length();
                        int end = trimmed.indexOf(REALM_COMMENT_SUFFIX, start);
                        if (end > -1) {
                            realmName = trimmed.substring(start, end);
                        }
                    } else {
                        if ( ! (trimmed.startsWith(COMMENT_PREFIX1) || trimmed.startsWith(COMMENT_PREFIX2)) ) {
                            String username = null;
                            StringBuilder builder = new StringBuilder();

                            CodePointIterator it = CodePointIterator.ofString(trimmed);
                            while (it.hasNext()) {
                                int cp = it.next();
                                if (cp == '\\' && it.hasNext()) { // escape
                                    //might be regular escape of regex like characters \\t \\! or unicode \\uxxxx
                                    int marker = it.next();
                                    if(marker != 'u'){
                                        builder.appendCodePoint(marker);
                                    } else {
                                        StringBuilder hex = new StringBuilder();
                                        try{
                                            hex.appendCodePoint(it.next());
                                            hex.appendCodePoint(it.next());
                                            hex.appendCodePoint(it.next());
                                            hex.appendCodePoint(it.next());
                                            builder.appendCodePoint((char)Integer.parseInt(hex.toString(),16));
                                        } catch(NoSuchElementException nsee){
                                            throw ElytronMessages.log.invalidUnicodeSequence(hex.toString(),nsee);
                                        }
                                    }
                                } else if (username == null && (cp == '=' || cp == ':')) { // username-password delimiter
                                    username = builder.toString().trim();
                                    builder = new StringBuilder();
                                } else {
                                    builder.appendCodePoint(cp);
                                }
                            }
                            if (username != null) { // end of line and delimiter was read
                                String password = builder.toString().trim();
                                accounts.put(username, new AccountEntry(username, password, groups.getProperty(username)));
                            }
                        }
                    }
                }
            }

            if (realmName == null) {
                if (defaultRealm != null || plainText) {
                    realmName = defaultRealm;
                } else {
                    throw log.noRealmFoundInProperties();
                }
            }
        }

        // users, which are in groups file only
        for (String userName : groups.stringPropertyNames()) {
            if (accounts.containsKey(userName) == false) {
                accounts.put(userName, new AccountEntry(userName, null, groups.getProperty(userName)));
            }
        }

        loadedState.set(new LoadedState(accounts, realmName, System.currentTimeMillis()));
    }

    /**
     * Get the time when the realm was last loaded.
     *
     * @return the time when the realm was last loaded (number of milliseconds since the standard base time)
     */
    public long getLoadTime() {
        return loadedState.get().getLoadTime();
    }

    /**
     * Obtain a new {@link Builder} capable of building a {@link LegacyPropertiesSecurityRealm}.
     *
     * @return a new {@link Builder} capable of building a {@link LegacyPropertiesSecurityRealm}.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * A builder for legacy properties security realms.
     */
    public static class Builder {

        private Supplier providers = INSTALLED_PROVIDERS;
        private InputStream usersStream;
        private InputStream groupsStream;
        private String defaultRealm = null;
        private boolean plainText;
        private String groupsAttribute = "groups";
        private Encoding hashEncoding = Encoding.HEX; // set to hex by default
        private Charset hashCharset = StandardCharsets.UTF_8; // set to UTF-8 by default

        Builder() {
        }

        /**
         * Set the supplier for {@link Provider} instanced for use bu the realm.
         *
         * @param providers the supplier for {@link Provider} instanced for use bu the realm.
         * @return this {@link Builder}
         */
        public Builder setProviders(Supplier providers) {
            Assert.checkNotNullParam("providers", providers);
            this.providers = providers;

            return this;
        }

        /**
         * Set the {@link InputStream} to use to load the users.
         *
         * @param usersStream the {@link InputStream} to use to load the users.
         * @return this {@link Builder}
         */
        public Builder setUsersStream(InputStream usersStream) {
            this.usersStream = usersStream;

            return this;
        }

        /**
         * Set the {@link InputStream} to use to load the group information.
         *
         * @param groupsStream the {@link InputStream} to use to load the group information.
         * @return this {@link Builder}
         */
        public Builder setGroupsStream(InputStream groupsStream) {
            this.groupsStream = groupsStream;

            return this;
        }

        /**
         * Where this realm returns an {@link AuthorizationIdentity} set the key on the Attributes that will be used to hold the
         * group membership information.
         *
         * @param groupsAttribute the key on the Attributes that will be used to hold the group membership information.
         * @return this {@link Builder}
         */
        public Builder setGroupsAttribute(final String groupsAttribute) {
            Assert.checkNotNullParam("groupsAttribute", groupsAttribute);
            this.groupsAttribute = groupsAttribute;

            return this;
        }


        /**
         * Set the default realm name to use if no realm name is discovered in the properties file.
         *
         * @param defaultRealm the default realm name if one is not discovered in the properties file.
         * @return this {@link Builder}
         */
        public Builder setDefaultRealm(String defaultRealm) {
            this.defaultRealm = defaultRealm;

            return this;
        }

        /**
         * Set format of users property file - if the passwords are stored in plain text.
         * Otherwise is HEX( MD5( username ":" realm ":" password ) ) expected.
         *
         * @param plainText if the passwords are stored in plain text.
         * @return this {@link Builder}
         */
        public Builder setPlainText(boolean plainText) {
            this.plainText = plainText;

            return this;
        }

        /**
         * Set the string format for the password in the properties file if they are not
         * stored in plain text. Set to hex by default.
         *
         * @param hashEncoding specifies the string format for the hashed password
         * @return this {@link Builder}
         */
        public Builder setHashEncoding(Encoding hashEncoding) {
            Assert.checkNotNullParam("hashEncoding", hashEncoding);
            this.hashEncoding = hashEncoding;

            return this;
        }

        /**
         * Set the character set to use when converting the password string
         * to a byte array. Set to UTF-8 by default.
         * @param hashCharset the name of the character set (must not be {@code null})
         * @return this {@link Builder}
         */
        public Builder setHashCharset(Charset hashCharset) {
            Assert.checkNotNullParam("hashCharset", hashCharset);
            this.hashCharset = hashCharset;

            return this;
        }

        /**
         * Builds the {@link LegacyPropertiesSecurityRealm}.
         * @return built {@link LegacyPropertiesSecurityRealm}
         * @throws IOException when loading of property files fails
         * @throws java.io.FileNotFoundException when property file does not exist
         * @throws RealmUnavailableException when property file of users does not contain realm name specification
         */
        public LegacyPropertiesSecurityRealm build() throws IOException {
            LegacyPropertiesSecurityRealm realm = new LegacyPropertiesSecurityRealm(this);
            realm.load(usersStream, groupsStream);

            return realm;
        }

    }

    private static class LoadedState {

        private final Map accounts;
        private final String realmName;
        private final long loadTime;

        private LoadedState(Map accounts, String realmName, long loadTime) {
            this.accounts = accounts;
            this.realmName = realmName;
            this.loadTime = loadTime;
        }

        public Map getAccounts() {
            return accounts;
        }

        public String getRealmName() {
            return realmName;
        }

        public long getLoadTime() {
            return loadTime;
        }

    }

    private static class AccountEntry {

        private final String name;
        private final String passwordRepresentation;
        private final Set groups;

        private AccountEntry(String name, String passwordRepresentation, String groups) {
            this.name = name;
            this.passwordRepresentation = passwordRepresentation;
            this.groups = convertGroups(groups);
        }

        private Set convertGroups(String groups) {
            if (groups == null) {
                return Collections.emptySet();
            }

            String[] groupArray = groups.split(",");
            Set groupsSet = new HashSet<>(groupArray.length);
            for (String current : groupArray) {
                String value = current.trim();
                if (value.length() > 0) {
                    groupsSet.add(value);
                }
            }

            return Collections.unmodifiableSet(groupsSet);
        }

        public String getName() {
            return name;
        }

        public String getPasswordRepresentation() {
            return passwordRepresentation;
        }

        public Set getGroups() {
            return groups;
        }
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy