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

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

Go to download

This artifact provides a single jar that contains all classes required to use remote Jakarta Enterprise Beans and Jakarta Messaging, including all dependencies. It is intended for use by those not using maven, maven users should just import the Jakarta Enterprise Beans and Jakarta Messaging 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: 35.0.0.Beta1
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 java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.DSYNC;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import org.wildfly.common.Assert;
import org.wildfly.common.bytes.ByteStringBuilder;
import org.wildfly.common.codec.Base32Alphabet;
import org.wildfly.common.codec.Base64Alphabet;
import org.wildfly.common.iteration.ByteIterator;
import org.wildfly.common.iteration.CodePointIterator;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.realm.IdentitySharedExclusiveLock.IdentityLock;
import org.wildfly.security.auth.server.ModifiableRealmIdentityIterator;
import org.wildfly.security.auth.server.ModifiableRealmIdentity;
import org.wildfly.security.auth.server.ModifiableSecurityRealm;
import org.wildfly.security.auth.server.NameRewriter;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.authz.Attributes;
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.credential.PublicKeyCredential;
import org.wildfly.security.credential.X509CertificateChainPublicCredential;
import org.wildfly.security.evidence.Evidence;
import org.wildfly.security.password.Password;
import org.wildfly.security.password.PasswordFactory;
import org.wildfly.security.password.interfaces.OneTimePassword;
import org.wildfly.security.password.spec.BasicPasswordSpecEncoding;
import org.wildfly.security.password.spec.OneTimePasswordSpec;
import org.wildfly.security.password.spec.PasswordSpec;
import org.wildfly.security.password.util.ModularCrypt;
import org.wildfly.security.permission.ElytronPermission;

/**
 * A simple filesystem-backed security realm.
 *
 * @author David M. Lloyd
 */
public final class FileSystemSecurityRealm implements ModifiableSecurityRealm, CacheableSecurityRealm {

    static final String ELYTRON_1_0 = "urn:elytron:1.0";
    static final String ELYTRON_1_0_1 = "urn:elytron:1.0.1";
    static final ElytronPermission CREATE_SECURITY_REALM = ElytronPermission.forName("createSecurityRealm");

    private final Path root;
    private final NameRewriter nameRewriter;
    private final int levels;
    private final boolean encoded;

    private final ConcurrentHashMap realmIdentityLocks = new ConcurrentHashMap<>();

    /**
     * Construct a new instance.
     *
     * Construction with enabled security manager requires {@code createSecurityRealm} {@link ElytronPermission}.
     *
     * @param root the root path of the identity store
     * @param nameRewriter the name rewriter to apply to looked up names
     * @param levels the number of levels of directory hashing to apply
     * @param encoded whether identity names should by BASE32 encoded before using as filename
     */
    public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final boolean encoded) {
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(CREATE_SECURITY_REALM);
        }
        this.root = root;
        this.nameRewriter = nameRewriter;
        this.levels = levels;
        this.encoded = encoded;
    }

    /**
     * Construct a new instance.
     *
     * @param root the root path of the identity store
     * @param nameRewriter the name rewriter to apply to looked up names
     * @param levels the number of levels of directory hashing to apply
     */
    public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels) {
        this(root, nameRewriter, levels, true);
    }

    /**
     * Construct a new instance.
     *
     * @param root the root path of the identity store
     * @param levels the number of levels of directory hashing to apply
     */
    public FileSystemSecurityRealm(final Path root, final int levels) {
        this(root, NameRewriter.IDENTITY_REWRITER, levels, true);
    }

    /**
     * Construct a new instance with 2 levels of hashing.
     *
     * @param root the root path of the identity store
     */
    public FileSystemSecurityRealm(final Path root) {
        this(root, NameRewriter.IDENTITY_REWRITER, 2, true);
    }

    private Path pathFor(String name) {
        assert name.codePointCount(0, name.length()) > 0;
        String normalizedName = name;

        if (encoded) {
            normalizedName = Normalizer.normalize(name, Normalizer.Form.NFKC)
                    .toLowerCase(Locale.ROOT)
                    .replaceAll("[^a-z0-9]", "_");
        }

        Path path = root;
        int idx = 0;
        for (int level = 0; level < levels; level ++) {
            int newIdx = normalizedName.offsetByCodePoints(idx, 1);
            path = path.resolve(normalizedName.substring(idx, newIdx));
            idx = newIdx;
            if (idx == normalizedName.length()) {
                break;
            }
        }

        if (encoded) {
            String base32 = ByteIterator.ofBytes(new ByteStringBuilder().append(name).toArray())
                    .base32Encode(Base32Alphabet.STANDARD, false).drainToString();
            name = normalizedName + "-" + base32;
        }

        return path.resolve(name + ".xml");
    }

    private String nameFor(Path path) {
        String fileName = path.toString();
        fileName = fileName.substring(0, fileName.length() - 4); // remove ".xml"
        if (encoded) {
            CodePointIterator it = CodePointIterator.ofString(fileName);
            it.delimitedBy('-').skipAll();
            it.next(); // skip '-'
            fileName = it.base32Decode(Base32Alphabet.STANDARD, false)
                    .asUtf8String().drainToString();
        }
        return fileName;
    }

    public RealmIdentity getRealmIdentity(final Principal principal) {
        return principal instanceof NamePrincipal ? getRealmIdentity(principal.getName(), false) : RealmIdentity.NON_EXISTENT;
    }

    public ModifiableRealmIdentity getRealmIdentityForUpdate(final Principal principal) {
        return principal instanceof NamePrincipal ? getRealmIdentity(principal.getName(), true) : ModifiableRealmIdentity.NON_EXISTENT;
    }

    @Override
    public void registerIdentityChangeListener(Consumer listener) {
        // no need to register the listener given that changes to identities are done through the realm
    }

    private ModifiableRealmIdentity getRealmIdentity(final String name, final boolean exclusive) {
        final String finalName = nameRewriter.rewriteName(name);
        if (finalName == null) {
            throw ElytronMessages.log.invalidName();
        }

        // Acquire the appropriate lock for the realm identity
        IdentitySharedExclusiveLock realmIdentityLock = getRealmIdentityLockForName(finalName);
        IdentityLock lock;
        if (exclusive) {
            lock = realmIdentityLock.lockExclusive();
        } else {
            lock = realmIdentityLock.lockShared();
        }
        return new Identity(finalName, pathFor(finalName), lock);
    }

    public ModifiableRealmIdentityIterator getRealmIdentityIterator() throws RealmUnavailableException {
        return subIterator(root, levels);
    }

    private ModifiableRealmIdentityIterator subIterator(final Path root, final int levels) {
        final DirectoryStream stream;
        final Iterator iterator;
        if (levels == 0) {
            try {
                stream = Files.newDirectoryStream(root, "*.xml");
                iterator = stream.iterator();
            } catch (IOException e) {
                ElytronMessages.log.debug("Unable to open directory", e);
                return ModifiableRealmIdentityIterator.emptyIterator();
            }
            return new ModifiableRealmIdentityIterator() {

                public boolean hasNext() {
                    if ( ! iterator.hasNext()) {
                        try {
                            close();
                        } catch (IOException e) {
                            ElytronMessages.log.debug("Unable to close the stream", e);
                        }
                    }
                    return iterator.hasNext();
                }

                public ModifiableRealmIdentity next() {
                    final Path path = iterator.next();
                    final String name = nameFor(path.getFileName());
                    return getRealmIdentityForUpdate(new NamePrincipal(name));
                }

                public void close() throws RealmUnavailableException {
                    try {
                        stream.close();
                    } catch (IOException e) {
                        ElytronMessages.log.debug("Unable to close the stream", e);
                    }
                }
            };
        } else {
            try {
                stream = Files.newDirectoryStream(root, entry -> {
                    final String fileName = entry.getFileName().toString();
                    return fileName.length() == 1 && !fileName.equals(".") && Files.isDirectory(entry);
                });
                iterator = stream.iterator();
            } catch (IOException e) {
                ElytronMessages.log.debug("Unable to open directory", e);
                return ModifiableRealmIdentityIterator.emptyIterator();
            }
            return new ModifiableRealmIdentityIterator() {
                private ModifiableRealmIdentityIterator subIterator;

                public boolean hasNext() {
                    for (;;) {
                        if (subIterator == null) {
                            if (! iterator.hasNext()) {
                                try {
                                    close();
                                } catch (IOException e) {
                                    ElytronMessages.log.debug("Unable to close the stream", e);
                                }
                                return false;
                            }
                            final Path path = iterator.next();
                            subIterator = subIterator(path, levels - 1);
                        } else if (subIterator.hasNext()) {
                            return true;
                        } else {
                            subIterator = null;
                        }
                    }
                }

                public ModifiableRealmIdentity next() {
                    if (! hasNext()) {
                        throw new NoSuchElementException();
                    }
                    return subIterator.next();
                }

                public void close() throws RealmUnavailableException {
                    try {
                        if (subIterator != null) subIterator.close();
                    } finally {
                        try {
                            stream.close();
                        } catch (IOException e) {
                            ElytronMessages.log.debug("Unable to close the stream", e);
                        }
                    }
                }
            };
        }
    }

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

    public SupportLevel getEvidenceVerifySupport(final Class evidenceType, final String algorithmName) throws RealmUnavailableException {
        return SupportLevel.POSSIBLY_SUPPORTED;
    }

    private IdentitySharedExclusiveLock getRealmIdentityLockForName(final String name) {
        IdentitySharedExclusiveLock realmIdentityLock = realmIdentityLocks.get(name);
        if (realmIdentityLock == null) {
            final IdentitySharedExclusiveLock newRealmIdentityLock = new IdentitySharedExclusiveLock();
            realmIdentityLock = realmIdentityLocks.putIfAbsent(name, newRealmIdentityLock);
            if (realmIdentityLock == null) {
                realmIdentityLock = newRealmIdentityLock;
            }
        }
        return realmIdentityLock;
    }

    @FunctionalInterface
    interface CredentialParseFunction {
        void parseCredential(String algorithm, String format, String body) throws RealmUnavailableException, XMLStreamException;
    }

    static class Identity implements ModifiableRealmIdentity {

        private static final String BASE64_FORMAT = "base64";
        private static final String MCF_FORMAT = "crypt";
        private static final String X509_FORMAT = "X.509";

        private final String name;
        private final Path path;
        private IdentityLock lock;

        Identity(final String name, final Path path, final IdentityLock lock) {
            this.name = name;
            this.path = path;
            this.lock = lock;
        }

        public Principal getRealmIdentityPrincipal() {
            return new NamePrincipal(name);
        }

        public SupportLevel getCredentialAcquireSupport(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
            Assert.checkNotNullParam("credentialType", credentialType);
            List credentials = loadCredentials();
            for (Credential credential : credentials) {
                if (credential.matches(credentialType, algorithmName, parameterSpec)) {
                    return SupportLevel.SUPPORTED;
                }
            }
            return SupportLevel.UNSUPPORTED;
        }

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

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

        public  C getCredential(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
            Assert.checkNotNullParam("credentialType", credentialType);
            List credentials = loadCredentials();
            for (Credential credential : credentials) {
                if (credential.matches(credentialType, algorithmName, parameterSpec)) {
                    return credentialType.cast(credential.clone());
                }
            }
            return null;
        }

        public SupportLevel getEvidenceVerifySupport(final Class evidenceType, final String algorithmName) throws RealmUnavailableException {
            Assert.checkNotNullParam("evidenceType", evidenceType);
            List credentials = loadCredentials();
            for (Credential credential : credentials) {
                if (credential.canVerify(evidenceType, algorithmName)) {
                    ElytronMessages.log.tracef("FileSystemSecurityRealm - evidence verification SUPPORTED: type = [%s]  algorithm = [%s]  credentials = [%d]", evidenceType, algorithmName, credentials.size());
                    return SupportLevel.SUPPORTED;
                }
            }
            ElytronMessages.log.tracef("FileSystemSecurityRealm - evidence verification UNSUPPORTED: type = [%s]  algorithm = [%s]  credentials = [%d]", evidenceType, algorithmName, credentials.size());
            return SupportLevel.UNSUPPORTED;
        }

        public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableException {
            Assert.checkNotNullParam("evidence", evidence);

            if (ElytronMessages.log.isTraceEnabled()) {
                final LoadedIdentity loadedIdentity = loadIdentity(false, true);
                ElytronMessages.log.tracef("Trying to authenticate identity %s using FileSystemSecurityRealm",
                        (loadedIdentity != null) ? loadedIdentity.getName() : "null");
            }

            List credentials = loadCredentials();
            ElytronMessages.log.tracef("FileSystemSecurityRealm - verification evidence [%s] against [%d] credentials...", evidence, credentials.size());
            for (Credential credential : credentials) {
                if (credential.canVerify(evidence)) {
                    boolean verified = credential.verify(evidence);
                    ElytronMessages.log.tracef("FileSystemSecurityRealm - verification against credential [%s] = %b", credential, verified);
                    return verified;
                }
            }
            ElytronMessages.log.tracef("FileSystemSecurityRealm - no credential able to verify evidence [%s]", evidence);
            return false;
        }

        private List loadCredentials() throws RealmUnavailableException {
            final LoadedIdentity loadedIdentity = loadIdentity(false, true);
            return loadedIdentity == null ? Collections.emptyList() : loadedIdentity.getCredentials();
        }

        public boolean exists() throws RealmUnavailableException {
            if (System.getSecurityManager() == null) {
                return Files.exists(path);
            }
            return AccessController.doPrivileged((PrivilegedAction) () -> Files.exists(path));
        }

        public void delete() throws RealmUnavailableException {
            if (System.getSecurityManager() == null) {
                deletePrivileged();
                return;
            }
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction) this::deletePrivileged);
            } catch (PrivilegedActionException e) {
                if (e.getException() instanceof RealmUnavailableException) {
                    throw (RealmUnavailableException) e.getException();
                }
                throw new RuntimeException(e.getException());
            }
        }

        private Void deletePrivileged() throws RealmUnavailableException {
            try {
                Files.delete(path);
                return null;
            } catch (NoSuchFileException e) {
                throw ElytronMessages.log.fileSystemRealmNotFound(name);
            } catch (IOException e) {
                throw ElytronMessages.log.fileSystemRealmDeleteFailed(name, e);
            }
        }

        private String tempSuffix() {
            final ThreadLocalRandom random = ThreadLocalRandom.current();
            char[] array = new char[12];
            for (int i = 0; i < array.length; i ++) {
                int idx = random.nextInt(36);
                if (idx < 26) {
                    array[i] = (char) ('A' + idx);
                } else {
                    array[i] = (char) ('0' + idx - 26);
                }
            }
            return new String(array);
        }

        private Path tempPath() {
            Path parent = path.getParent();
            File file = parent.toFile();
            if (!file.exists()) {
                file.mkdirs();
            }
            return parent.resolve(path.getFileName().toString() + '.' + tempSuffix());
        }

        public void create() throws RealmUnavailableException {
            if (System.getSecurityManager() == null) {
                createPrivileged();
                return;
            }
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction) this::createPrivileged);
            } catch (PrivilegedActionException e) {
                if (e.getException() instanceof RealmUnavailableException) {
                    throw (RealmUnavailableException) e.getException();
                }
                throw new RuntimeException(e.getException());
            }
        }

        private Void createPrivileged() throws RealmUnavailableException {
            for (;;) {
                final Path tempPath = tempPath();
                final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();
                try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(tempPath, WRITE, CREATE_NEW, DSYNC))) {
                    try (AutoCloseableXMLStreamWriterHolder holder = new AutoCloseableXMLStreamWriterHolder(xmlOutputFactory.createXMLStreamWriter(outputStream))) {
                        final XMLStreamWriter streamWriter = holder.getXmlStreamWriter();
                        // create empty identity
                        streamWriter.writeStartDocument();
                        streamWriter.writeCharacters("\n");
                        streamWriter.writeStartElement("identity");
                        // Continue to write using 1.0 as not using any features added in 1.0.1
                        streamWriter.writeDefaultNamespace(ELYTRON_1_0);
                        streamWriter.writeEndElement();
                        streamWriter.writeEndDocument();
                    } catch (XMLStreamException e) {
                        throw ElytronMessages.log.fileSystemRealmFailedToWrite(tempPath, name, e);
                    }
                } catch (FileAlreadyExistsException ignored) {
                    // try a new name
                    continue;
                } catch (IOException e) {
                    throw ElytronMessages.log.fileSystemRealmFailedToOpen(tempPath, name, e);
                }
                try {
                    Files.createLink(path, tempPath);
                } catch (FileAlreadyExistsException e) {
                    try {
                        Files.delete(tempPath);
                    } catch (IOException e2) {
                        e.addSuppressed(e2);
                    }
                    throw ElytronMessages.log.fileSystemRealmAlreadyExists(name, e);
                } catch (IOException e) {
                    throw ElytronMessages.log.fileSystemRealmFailedToWrite(tempPath, name, e);
                }
                try {
                    Files.delete(tempPath);
                } catch (IOException ignored) {
                    // nothing we can do
                }
                return null;
            }
        }

        public void setCredentials(final Collection credentials) throws RealmUnavailableException {
            Assert.checkNotNullParam("credential", credentials);
            final LoadedIdentity loadedIdentity = loadIdentity(false, false);
            if (loadedIdentity == null) {
                throw ElytronMessages.log.fileSystemRealmNotFound(name);
            }

            final LoadedIdentity newIdentity = new LoadedIdentity(name, new ArrayList<>(credentials), loadedIdentity.getAttributes());
            replaceIdentity(newIdentity);
        }

        public void setAttributes(final Attributes attributes) throws RealmUnavailableException {
            Assert.checkNotNullParam("attributes", attributes);
            final LoadedIdentity loadedIdentity = loadIdentity(false, true);
            if (loadedIdentity == null) {
                throw ElytronMessages.log.fileSystemRealmNotFound(name);
            }
            final LoadedIdentity newIdentity = new LoadedIdentity(name, loadedIdentity.getCredentials(), attributes);
            replaceIdentity(newIdentity);
        }

        @Override
        public Attributes getAttributes() throws RealmUnavailableException {
            final LoadedIdentity loadedIdentity = loadIdentity(true, false);
            if (loadedIdentity == null) {
                throw ElytronMessages.log.fileSystemRealmNotFound(name);
            }
            return loadedIdentity.getAttributes().asReadOnly();
        }

        private void replaceIdentity(final LoadedIdentity newIdentity) throws RealmUnavailableException {
            if (System.getSecurityManager() == null) {
                replaceIdentityPrivileged(newIdentity);
                return;
            }
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction) () -> replaceIdentityPrivileged(newIdentity));
            } catch (PrivilegedActionException e) {
                if (e.getException() instanceof RealmUnavailableException) {
                    throw (RealmUnavailableException) e.getException();
                }
                throw new RuntimeException(e.getException());
            }
        }

        private Void replaceIdentityPrivileged(final LoadedIdentity newIdentity) throws RealmUnavailableException {
            for (;;) {
                final Path tempPath = tempPath();
                try {
                    final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();
                    try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(tempPath, WRITE, CREATE_NEW, DSYNC))) {
                        try (AutoCloseableXMLStreamWriterHolder holder = new AutoCloseableXMLStreamWriterHolder(xmlOutputFactory.createXMLStreamWriter(outputStream))) {
                            writeIdentity(holder.getXmlStreamWriter(), newIdentity);
                        } catch (XMLStreamException | InvalidKeySpecException | NoSuchAlgorithmException | CertificateEncodingException e) {
                            throw ElytronMessages.log.fileSystemRealmFailedToWrite(tempPath, name, e);
                        }
                    } catch (FileAlreadyExistsException ignored) {
                        // try a new name
                        continue;
                    } catch (IOException e) {
                        try {
                            Files.deleteIfExists(tempPath);
                        } catch (IOException e2) {
                            e.addSuppressed(e2);
                        }
                        throw ElytronMessages.log.fileSystemRealmFailedToOpen(tempPath, name, e);
                    }
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        throw ElytronMessages.log.fileSystemUpdatedFailed(path.toAbsolutePath().toString(), e);
                    }
                    try {
                        Files.createLink(path, tempPath);
                    } catch (FileAlreadyExistsException e) {
                        try {
                            Files.deleteIfExists(tempPath);
                        } catch (IOException e2) {
                            e.addSuppressed(e2);
                        }
                        throw ElytronMessages.log.fileSystemRealmAlreadyExists(name, e);
                    } catch (IOException e) {
                        throw ElytronMessages.log.fileSystemRealmFailedToWrite(tempPath, name, e);
                    }
                    try {
                        Files.delete(tempPath);
                    } catch (IOException ignored) {
                        // nothing we can do
                    }
                    return null;
                } catch (Throwable t) {
                    try {
                        Files.delete(tempPath);
                    } catch (IOException e) {
                        t.addSuppressed(e);
                    }
                    throw t;
                }
            }
        }

        private void writeIdentity(final XMLStreamWriter streamWriter, final LoadedIdentity newIdentity) throws XMLStreamException, InvalidKeySpecException, NoSuchAlgorithmException, CertificateEncodingException {
            streamWriter.writeStartDocument();
            streamWriter.writeCharacters("\n");
            streamWriter.writeStartElement("identity");
            // Continue to write using 1.0 as not using any features added in 1.0.1
            streamWriter.writeDefaultNamespace(ELYTRON_1_0);

            if (newIdentity.getCredentials().size() > 0) {
                streamWriter.writeCharacters("\n    ");
                streamWriter.writeStartElement("credentials");
                for (Credential credential : newIdentity.getCredentials()) {
                    streamWriter.writeCharacters("\n        ");
                    if (credential instanceof PasswordCredential) {
                        Password password = ((PasswordCredential) credential).getPassword();
                        if (password instanceof OneTimePassword) {
                            final OneTimePassword otp = (OneTimePassword) password;
                            streamWriter.writeStartElement("otp");
                            streamWriter.writeAttribute("algorithm", otp.getAlgorithm());
                            streamWriter.writeAttribute("hash", ByteIterator.ofBytes(otp.getHash()).base64Encode().drainToString());
                            streamWriter.writeAttribute("seed", ByteIterator.ofBytes(otp.getSeed().getBytes(StandardCharsets.US_ASCII)).base64Encode().drainToString());
                            streamWriter.writeAttribute("sequence", Integer.toString(otp.getSequenceNumber()));
                            streamWriter.writeEndElement();
                        } else {
                            streamWriter.writeStartElement("password");
                            String format;
                            String algorithm = password.getAlgorithm();
                            String passwordString;
                            byte[] encoded = BasicPasswordSpecEncoding.encode(password);

                            if (encoded != null) {
                                format = BASE64_FORMAT;
                                passwordString = ByteIterator.ofBytes(encoded).base64Encode().drainToString();
                            } else {
                                format = MCF_FORMAT;
                                passwordString = ModularCrypt.encodeAsString(password);
                            }

                            streamWriter.writeAttribute("algorithm", algorithm);
                            streamWriter.writeAttribute("format", format);
                            streamWriter.writeCharacters(passwordString);
                            streamWriter.writeEndElement();
                        }
                    }
                }
                streamWriter.writeCharacters("\n    ");
                streamWriter.writeEndElement();
            }
            final Iterator entryIter = newIdentity.getAttributes().entries().iterator();
            if (entryIter.hasNext()) {
                streamWriter.writeCharacters("\n    ");
                streamWriter.writeStartElement("attributes");
                do {
                    final Attributes.Entry entry = entryIter.next();
                    for (String value : entry) {
                        streamWriter.writeCharacters("\n        ");
                        streamWriter.writeStartElement("attribute");
                        streamWriter.writeAttribute("name", entry.getKey());
                        streamWriter.writeAttribute("value", value);
                        streamWriter.writeEndElement();
                    }
                } while (entryIter.hasNext());
                streamWriter.writeCharacters("\n    ");
                streamWriter.writeEndElement();
            }
            streamWriter.writeEndElement();
            streamWriter.writeEndDocument();
        }

        public void dispose() {
            // Release the lock for this realm identity
            IdentityLock identityLock = lock;
            lock = null;
            if (identityLock != null) {
                identityLock.release();
            }
        }

        public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException {
            final LoadedIdentity loadedIdentity = loadIdentity(true, false);
            return loadedIdentity == null ? AuthorizationIdentity.EMPTY : AuthorizationIdentity.basicIdentity(loadedIdentity.getAttributes());
        }

        private LoadedIdentity loadIdentity(final boolean skipCredentials, final boolean skipAttributes) throws RealmUnavailableException {
            if (System.getSecurityManager() == null) {
                return loadIdentityPrivileged(skipCredentials, skipAttributes);
            }
            try {
                return AccessController.doPrivileged((PrivilegedExceptionAction) () -> loadIdentityPrivileged(skipCredentials, skipAttributes));
            } catch (PrivilegedActionException e) {
                if (e.getException() instanceof RealmUnavailableException) {
                    throw (RealmUnavailableException) e.getException();
                }
                throw new RuntimeException(e.getException());
            }
        }

        private LoadedIdentity loadIdentityPrivileged(final boolean skipCredentials, final boolean skipAttributes) throws RealmUnavailableException {
            try (InputStream inputStream = Files.newInputStream(path, READ)) {
                final XMLInputFactory inputFactory = XMLInputFactory.newFactory();
                inputFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
                inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
                inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
                inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
                try (final AutoCloseableXMLStreamReaderHolder holder = new AutoCloseableXMLStreamReaderHolder(inputFactory.createXMLStreamReader(inputStream, "UTF-8"))) {
                    final XMLStreamReader streamReader = holder.getXmlStreamReader();
                    return parseIdentity(streamReader, skipCredentials, skipAttributes);
                } catch (XMLStreamException e) {
                    throw ElytronMessages.log.fileSystemRealmFailedToRead(path, name, e);
                }
            } catch (NoSuchFileException | FileNotFoundException ignored) {
                return null;
            } catch (IOException e) {
                throw ElytronMessages.log.fileSystemRealmFailedToOpen(path, name, e);
            }
        }

        private LoadedIdentity parseIdentity(final XMLStreamReader streamReader, final boolean skipCredentials, final boolean skipAttributes) throws RealmUnavailableException, XMLStreamException {
            final int tag = streamReader.nextTag();
            if (tag != START_ELEMENT || ! validNamespace(streamReader.getNamespaceURI()) || ! "identity".equals(streamReader.getLocalName())) {
                throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
            }
            return parseIdentityContents(streamReader, skipCredentials, skipAttributes);
        }

        private LoadedIdentity parseIdentityContents(final XMLStreamReader streamReader, final boolean skipCredentials, final boolean skipAttributes) throws RealmUnavailableException, XMLStreamException {
            final int attributeCount = streamReader.getAttributeCount();
            if (attributeCount > 0) {
                throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
            }
            List credentials = Collections.emptyList();
            Attributes attributes = Attributes.EMPTY;
            boolean gotCredentials = false;
            boolean gotAttributes = false;
            for (;;) {
                if (streamReader.isEndElement()) {
                    if (attributes == Attributes.EMPTY && !skipAttributes) {
                        //Since this could be a use-case wanting to modify the attributes, make sure that we have a
                        //modifiable version of Attributes;
                        attributes = new MapAttributes();
                    }
                    return new LoadedIdentity(name, credentials, attributes);
                }
                if (! validNamespace(streamReader.getNamespaceURI())) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
                if (! gotCredentials && "credentials".equals(streamReader.getLocalName())) {
                    gotCredentials = true;
                    if (skipCredentials) {
                        consumeContent(streamReader);
                    } else {
                        credentials = parseCredentials(streamReader);
                    }
                } else if (! gotAttributes && "attributes".equals(streamReader.getLocalName())) {
                    gotAttributes = true;
                    if (skipAttributes) {
                        consumeContent(streamReader);
                    } else {
                        attributes = parseAttributes(streamReader);
                    }
                }
                streamReader.nextTag();
            }
        }

        private List parseCredentials(final XMLStreamReader streamReader) throws RealmUnavailableException, XMLStreamException {
            final int attributeCount = streamReader.getAttributeCount();
            if (attributeCount > 0) {
                throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
            }
            if (streamReader.nextTag() == END_ELEMENT) {
                return Collections.emptyList();
            }
            List credentials = new ArrayList<>();
            do {
                if (! validNamespace(streamReader.getNamespaceURI())) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
                if ("password".equals(streamReader.getLocalName())) {
                    parsePassword(credentials, streamReader);
                } else if ("public-key".equals(streamReader.getLocalName())) {
                    parsePublicKey(credentials, streamReader);
                } else if ("certificate".equals(streamReader.getLocalName())) {
                    parseCertificate(credentials, streamReader);
                } else if ("otp".equals(streamReader.getLocalName())) {
                    parseOtp(credentials, streamReader);
                } else {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
            } while (streamReader.nextTag() != END_ELEMENT);
            return credentials;
        }

        private void parseCredential(final XMLStreamReader streamReader, CredentialParseFunction function) throws RealmUnavailableException, XMLStreamException {
            final int attributeCount = streamReader.getAttributeCount();
            String name = null;
            String algorithm = null;
            String format = null;
            for (int i = 0; i < attributeCount; i ++) {
                String namespace = streamReader.getAttributeNamespace(i);
                if (namespace != null && !namespace.equals("")) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
                final String localName = streamReader.getAttributeLocalName(i);
                if ("name".equals(localName)) {
                    name = streamReader.getAttributeValue(i);
                } else if ("algorithm".equals(localName)) {
                    algorithm = streamReader.getAttributeValue(i);
                } else if ("format".equals(localName)) {
                    format = streamReader.getAttributeValue(i);
                } else {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
            }
            final String text = streamReader.getElementText().trim();
            function.parseCredential(algorithm, format, text);
        }

        private void parseCertificate(final List credentials, final XMLStreamReader streamReader) throws RealmUnavailableException, XMLStreamException {
            parseCredential(streamReader, (algorithm, format, text) -> {
                if (algorithm == null) algorithm = "X.509";
                if (format == null) format = X509_FORMAT;
                try {
                    final CertificateFactory certificateFactory = CertificateFactory.getInstance(algorithm);
                    credentials.add(new X509CertificateChainPublicCredential((X509Certificate) certificateFactory.generateCertificate(
                        CodePointIterator.ofString(text).base64Decode().asInputStream())));
                } catch (CertificateException | ClassCastException e) {
                    throw ElytronMessages.log.fileSystemRealmCertificateReadError(format, path, streamReader.getLocation().getLineNumber(), name);
                }
            });
        }

        private void parsePublicKey(final List credentials, final XMLStreamReader streamReader) throws RealmUnavailableException, XMLStreamException {
            parseCredential(streamReader, (algorithm, format, text) -> {
                if (algorithm == null) {
                    throw ElytronMessages.log.fileSystemRealmMissingAttribute("algorithm", path, streamReader.getLocation().getLineNumber(), name);
                }
                if (format == null) {
                    format = X509_FORMAT;
                } else if (!X509_FORMAT.equals(format)) {
                    throw ElytronMessages.log.fileSystemRealmUnsupportedKeyFormat(format, path, streamReader.getLocation().getLineNumber(), name);
                }
                try {
                    KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
                    credentials.add(new PublicKeyCredential(keyFactory.generatePublic(new PKCS8EncodedKeySpec(CodePointIterator.ofString(text).base64Decode().drain()))));
                } catch (NoSuchAlgorithmException e) {
                    throw ElytronMessages.log.fileSystemRealmUnsupportedKeyAlgorithm(format, path, streamReader.getLocation().getLineNumber(), name, e);
                } catch (InvalidKeySpecException e) {
                    throw ElytronMessages.log.fileSystemRealmUnsupportedKeyFormat(format, path, streamReader.getLocation().getLineNumber(), name);
                }
            });
        }

        private void parsePassword(final List credentials, final XMLStreamReader streamReader) throws XMLStreamException, RealmUnavailableException {
            parseCredential(streamReader, (algorithm, format, text) -> {
                try {
                    if (BASE64_FORMAT.equals(format)) {
                        if (algorithm == null) {
                            throw ElytronMessages.log.fileSystemRealmMissingAttribute("algorithm", path, streamReader.getLocation().getLineNumber(), name);
                        }
                        byte[] passwordBytes = CodePointIterator.ofChars(text.toCharArray()).base64Decode().drain();
                        PasswordFactory passwordFactory = PasswordFactory.getInstance(algorithm);
                        PasswordSpec passwordSpec = BasicPasswordSpecEncoding.decode(passwordBytes);

                        if (passwordSpec != null) {
                            credentials.add(new PasswordCredential(passwordFactory.generatePassword(passwordSpec)));
                        } else {
                            throw ElytronMessages.log.fileSystemRealmInvalidPasswordAlgorithm(algorithm, path, streamReader.getLocation().getLineNumber(), name);
                        }
                    } else if (MCF_FORMAT.equals(format)) {
                        credentials.add(new PasswordCredential(ModularCrypt.decode(text)));
                    } else {
                        throw ElytronMessages.log.fileSystemRealmInvalidPasswordFormat(format, path, streamReader.getLocation().getLineNumber(), name);
                    }
                } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
            });
        }

        private void parseOtp(final List credentials, final XMLStreamReader streamReader) throws XMLStreamException, RealmUnavailableException {
            String name = null;
            String algorithm = null;
            byte[] hash = null;
            String seed = null;
            int sequenceNumber = 0;

            final int attributeCount = streamReader.getAttributeCount();
            for (int i = 0; i < attributeCount; i ++) {
                String namespace = streamReader.getAttributeNamespace(i);
                if (namespace != null && !namespace.equals("")) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
                final String localName = streamReader.getAttributeLocalName(i);
                if ("name".equals(localName)) {
                    name = streamReader.getAttributeValue(i);
                } else if ("algorithm".equals(localName)) {
                    algorithm = streamReader.getAttributeValue(i);
                } else if ("hash".equals(localName)) {
                    hash = CodePointIterator.ofString(streamReader.getAttributeValue(i)).base64Decode(Base64Alphabet.STANDARD, false).drain();
                } else if ("seed".equals(localName)) {
                    seed = new String(CodePointIterator.ofString(streamReader.getAttributeValue(i)).base64Decode(Base64Alphabet.STANDARD, false).drain(), StandardCharsets.US_ASCII);
                } else if ("sequence".equals(localName)) {
                    sequenceNumber = Integer.parseInt(streamReader.getAttributeValue(i));
                } else {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
            }

            if (streamReader.nextTag() != END_ELEMENT) {
                throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
            }

            try {
                if (algorithm == null) {
                    throw ElytronMessages.log.fileSystemRealmMissingAttribute("algorithm", path, streamReader.getLocation().getLineNumber(), name);
                }
                PasswordFactory passwordFactory = PasswordFactory.getInstance(algorithm);
                Password password = passwordFactory.generatePassword(new OneTimePasswordSpec(hash, seed, sequenceNumber));
                credentials.add(new PasswordCredential(password));
            } catch (InvalidKeySpecException e) {
                throw ElytronMessages.log.fileSystemRealmInvalidOtpDefinition(path, streamReader.getLocation().getLineNumber(), name, e);
            } catch (NoSuchAlgorithmException e) {
                throw ElytronMessages.log.fileSystemRealmInvalidOtpAlgorithm(algorithm, path, streamReader.getLocation().getLineNumber(), name, e);
            }
        }

        private Attributes parseAttributes(final XMLStreamReader streamReader) throws RealmUnavailableException, XMLStreamException {
            final int attributeCount = streamReader.getAttributeCount();
            if (attributeCount > 0) {
                throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
            }
            int tag = streamReader.nextTag();
            if (tag == END_ELEMENT) {
                return Attributes.EMPTY;
            }
            Attributes attributes = new MapAttributes();
            do {
                if (! validNamespace(streamReader.getNamespaceURI())) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
                if ("attribute".equals(streamReader.getLocalName())) {
                    parseAttribute(streamReader, attributes);
                } else {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name);
                }
            } while (streamReader.nextTag() == START_ELEMENT);
            return attributes;
        }

        private void parseAttribute(final XMLStreamReader streamReader, final Attributes attributes) throws XMLStreamException, RealmUnavailableException {
            String name = null;
            String value = null;
            final int attributeCount = streamReader.getAttributeCount();
            for (int i = 0; i < attributeCount; i++) {
                String namespace = streamReader.getAttributeNamespace(i);
                if (namespace != null && !namespace.equals("")) {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), this.name);
                }
                if ("name".equals(streamReader.getAttributeLocalName(i))) {
                    name = streamReader.getAttributeValue(i);
                } else if ("value".equals(streamReader.getAttributeLocalName(i))) {
                    value = streamReader.getAttributeValue(i);
                } else {
                    throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), this.name);
                }
            }
            if (name == null) {
                throw ElytronMessages.log.fileSystemRealmMissingAttribute("name", path, streamReader.getLocation().getLineNumber(), this.name);
            }
            if (value == null) {
                throw ElytronMessages.log.fileSystemRealmMissingAttribute("value", path, streamReader.getLocation().getLineNumber(), this.name);
            }
            attributes.addLast(name, value);
            if (streamReader.nextTag() != END_ELEMENT) {
                throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), this.name);
            }
        }

        private void consumeContent(final XMLStreamReader reader) throws XMLStreamException {
            while (reader.hasNext()) {
                switch (reader.next()) {
                    case START_ELEMENT: {
                        consumeContent(reader);
                        break;
                    }
                    case END_ELEMENT: {
                        return;
                    }
                }
            }
        }

        private boolean validNamespace(final String namespace) {
            return ELYTRON_1_0.equals(namespace) || ELYTRON_1_0_1.equals(namespace);
        }
    }

    static final class LoadedIdentity {
        private final String name;
        private final List credentials;
        private final Attributes attributes;

        LoadedIdentity(final String name, final List credentials, final Attributes attributes) {
            this.name = name;
            this.credentials = credentials;
            this.attributes = attributes;
        }

        public String getName() {
            return name;
        }

        public Attributes getAttributes() {
            return attributes;
        }

        List getCredentials() {
            return credentials;
        }
    }

    static class AutoCloseableXMLStreamReaderHolder implements AutoCloseable {
        private final XMLStreamReader xmlStreamReader;

        AutoCloseableXMLStreamReaderHolder(final XMLStreamReader xmlStreamReader) {
            this.xmlStreamReader = xmlStreamReader;
        }

        public void close() throws XMLStreamException {
            xmlStreamReader.close();
        }

        public XMLStreamReader getXmlStreamReader() {
            return xmlStreamReader;
        }
    }

    static class AutoCloseableXMLStreamWriterHolder implements AutoCloseable {
        private final XMLStreamWriter xmlStreamWriter;

        AutoCloseableXMLStreamWriterHolder(final XMLStreamWriter xmlStreamWriter) {
            this.xmlStreamWriter = xmlStreamWriter;
        }

        public void close() throws XMLStreamException {
            xmlStreamWriter.close();
        }

        public XMLStreamWriter getXmlStreamWriter() {
            return xmlStreamWriter;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy