001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.auth.xacml;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.IOException;
023import java.net.URL;
024
025import javax.inject.Inject;
026import javax.jcr.Node;
027import javax.jcr.RepositoryException;
028import javax.jcr.Session;
029import javax.jcr.nodetype.NodeType;
030import javax.jcr.nodetype.NodeTypeIterator;
031import javax.jcr.nodetype.NodeTypeTemplate;
032
033import org.fcrepo.http.commons.session.SessionFactory;
034import org.fcrepo.kernel.api.FedoraSession;
035import org.fcrepo.kernel.api.exception.InvalidChecksumException;
036import org.fcrepo.kernel.api.models.FedoraBinary;
037import org.fcrepo.kernel.api.services.BinaryService;
038
039import org.apache.commons.io.FileUtils;
040import org.modeshape.jcr.api.nodetype.NodeTypeManager;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044import com.google.common.collect.ImmutableList;
045
046import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession;
047
048/**
049 * Sets up node types and default policies for the XACML Authorization Delegate.
050 *
051 * @author Gregory Jansen
052 */
053public class XACMLWorkspaceInitializer {
054
055    /**
056     * Class-level logger.
057     */
058    private static final Logger LOGGER = LoggerFactory.getLogger(XACMLWorkspaceInitializer.class);
059
060    /**
061     * Fedora's ModeShape session factory.
062     */
063    @Inject
064    private SessionFactory sessionFactory;
065
066    @Inject
067    private BinaryService binaryService;
068
069    private final File initialPoliciesDirectory;
070
071    private final File initialRootPolicyFile;
072
073    /**
074     * Constructor
075     *
076     * @param initialPoliciesDirectory of default policies
077     * @param initialRootPolicyFile    defining root policy
078     */
079    public XACMLWorkspaceInitializer(final File initialPoliciesDirectory, final File initialRootPolicyFile) {
080        if (null == initialPoliciesDirectory) {
081            throw new IllegalArgumentException("InitialPolicyDirectory is null!");
082        }
083        if (null == initialPoliciesDirectory.list() || 0 == initialPoliciesDirectory.list().length) {
084            throw new IllegalArgumentException("InitialPolicyDirectory does not exist or is empty! " +
085                                                       initialPoliciesDirectory.getAbsolutePath());
086        }
087        if (null == initialRootPolicyFile || !initialRootPolicyFile.exists()) {
088            throw new IllegalArgumentException("InitialRootPolicyFile is null or does not exist!");
089        }
090
091        this.initialPoliciesDirectory = initialPoliciesDirectory;
092        this.initialRootPolicyFile = initialRootPolicyFile;
093    }
094
095    /**
096     * Initializes default policies.
097     */
098    public void init() {
099        doInit(false);
100    }
101
102    /**
103     * Initializes node types and default policies - for Integration Tests!
104     */
105    public void initTest() {
106        doInit(true);
107    }
108
109    private void doInit(final boolean test) {
110        // Do not "registerNodeTypes" because the xacml-policy.cnd introduces a cyclical dependency with the main
111        //  fedora-node-types.cnd that causes an exception on repository restart.
112        if (test) {
113            registerNodeTypes();
114        }
115        loadInitialPolicies();
116        linkRootToPolicy();
117    }
118
119    private void registerNodeTypes() {
120        Session session = null;
121        try {
122            session = getJcrSession(sessionFactory.getInternalSession());
123            final NodeTypeManager mgr = (NodeTypeManager) session.getWorkspace().getNodeTypeManager();
124            final URL cnd = XACMLWorkspaceInitializer.class.getResource("/cnd/xacml-policy.cnd");
125            final NodeTypeIterator nti = mgr.registerNodeTypes(cnd, true);
126            while (nti.hasNext()) {
127                final NodeType nt = nti.nextNodeType();
128                LOGGER.debug("registered node type: {}", nt.getName());
129            }
130
131            // Add "authz:xacmlAssignable" mixin to "fedora:Resource" type
132            final NodeType nodeType = mgr.getNodeType("fedora:Resource");
133            final NodeTypeTemplate nodeTypeTemplate = mgr.createNodeTypeTemplate(nodeType);
134            final String[] superTypes = nodeType.getDeclaredSupertypeNames();
135            final ImmutableList.Builder<String> listBuilder = ImmutableList.builder();
136            listBuilder.add(superTypes);
137            listBuilder.add("authz:xacmlAssignable");
138            final ImmutableList<String> newSuperTypes = listBuilder.build();
139            nodeTypeTemplate.setDeclaredSuperTypeNames(newSuperTypes.toArray(new String[newSuperTypes.size()]));
140
141            mgr.registerNodeType(nodeTypeTemplate, true);
142
143            session.save();
144            LOGGER.debug("Registered XACML policy node types");
145        } catch (final RepositoryException | IOException e) {
146            throw new Error("Cannot register XACML policy node types", e);
147        } finally {
148            if (session != null) {
149                session.logout();
150            }
151        }
152    }
153
154    /**
155     * Create nodes for the default XACML policy set. Policies are created at paths according to their IDs.
156     */
157    private void loadInitialPolicies() {
158        FedoraSession session = null;
159        try {
160            session = sessionFactory.getInternalSession();
161            for (final File p : initialPoliciesDirectory.listFiles()) {
162                final String id = PolicyUtil.getID(FileUtils.openInputStream(p));
163                final String repoPath = PolicyUtil.getPathForId(id);
164                final FedoraBinary binary = binaryService.findOrCreate(session, repoPath);
165                try (FileInputStream stream = new FileInputStream(p)) {
166                    binary.setContent(stream, "application/xml", null, p.getName(), null);
167                }
168                LOGGER.info("Add initial policy {} at {}", p.getAbsolutePath(), binary.getPath());
169            }
170            session.commit();
171        } catch (final InvalidChecksumException | IOException e) {
172            throw new Error("Cannot create default root policies", e);
173        } finally {
174            if (session != null) {
175                session.expire();
176            }
177        }
178    }
179
180    /**
181     * Set the policy that is effective at the root node.
182     */
183    private void linkRootToPolicy() {
184        Session session = null;
185        try {
186            session = getJcrSession(sessionFactory.getInternalSession());
187            session.getRootNode().addMixin("authz:xacmlAssignable");
188            final String id = PolicyUtil.getID(FileUtils.openInputStream(initialRootPolicyFile));
189            final String repoPath = PolicyUtil.getPathForId(id);
190            final Node globalPolicy = session.getNode(repoPath);
191            session.getRootNode().setProperty("authz:policy", globalPolicy);
192            session.save();
193        } catch (final RepositoryException | IOException e) {
194            throw new Error("Cannot configure root mix-in or policy", e);
195        } finally {
196            if (session != null) {
197                session.logout();
198            }
199        }
200    }
201}