69257225d1a8effc992e9ebf76432817c1806d08
[shibboleth/cpp-sp.git] / plugins / TransformAttributeResolver.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * TransformAttributeResolver.cpp
23  * 
24  * Attribute Resolver plugin for transforming input values.
25  */
26
27 #include "internal.h"
28
29 #include <algorithm>
30 #include <boost/shared_ptr.hpp>
31 #include <boost/tuple/tuple.hpp>
32 #include <shibsp/exceptions.h>
33 #include <shibsp/SessionCache.h>
34 #include <shibsp/attribute/SimpleAttribute.h>
35 #include <shibsp/attribute/resolver/AttributeResolver.h>
36 #include <shibsp/attribute/resolver/ResolutionContext.h>
37 #include <xmltooling/XMLToolingConfig.h>
38 #include <xmltooling/util/XMLHelper.h>
39 #include <xercesc/util/XMLUniDefs.hpp>
40 #include <xercesc/util/regx/RegularExpression.hpp>
41
42 using namespace shibsp;
43 using namespace xmltooling;
44 using namespace xercesc;
45 using namespace boost;
46 using namespace std;
47
48 namespace shibsp {
49
50     class SHIBSP_DLLLOCAL TransformContext : public ResolutionContext
51     {
52     public:
53         TransformContext(const Session& session) : m_inputAttributes(&session.getAttributes()) {
54         }
55
56         TransformContext(const vector<shibsp::Attribute*>* attributes) : m_inputAttributes(attributes) {
57         }
58
59         ~TransformContext() {
60             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<shibsp::Attribute>());
61         }
62
63         const vector<shibsp::Attribute*>* getInputAttributes() const {
64             return m_inputAttributes;
65         }
66         vector<shibsp::Attribute*>& getResolvedAttributes() {
67             return m_attributes;
68         }
69         vector<opensaml::Assertion*>& getResolvedAssertions() {
70             return m_assertions;
71         }
72
73     private:
74         const vector<shibsp::Attribute*>* m_inputAttributes;
75         vector<shibsp::Attribute*> m_attributes;
76         static vector<opensaml::Assertion*> m_assertions;   // empty dummy
77     };
78
79
80     class SHIBSP_DLLLOCAL TransformAttributeResolver : public AttributeResolver
81     {
82     public:
83         TransformAttributeResolver(const DOMElement* e);
84         virtual ~TransformAttributeResolver() {}
85
86         Lockable* lock() {
87             return this;
88         }
89         void unlock() {
90         }
91
92         ResolutionContext* createResolutionContext(
93             const Application& application,
94             const opensaml::saml2md::EntityDescriptor* issuer,
95             const XMLCh* protocol,
96             const opensaml::saml2::NameID* nameid=nullptr,
97             const XMLCh* authncontext_class=nullptr,
98             const XMLCh* authncontext_decl=nullptr,
99             const vector<const opensaml::Assertion*>* tokens=nullptr,
100             const vector<shibsp::Attribute*>* attributes=nullptr
101             ) const {
102             // Make sure new method gets run.
103             return createResolutionContext(application, nullptr, issuer, protocol, nameid, authncontext_class, authncontext_decl, tokens, attributes);
104         }
105
106         ResolutionContext* createResolutionContext(
107             const Application& application,
108             const GenericRequest* request,
109             const opensaml::saml2md::EntityDescriptor* issuer,
110             const XMLCh* protocol,
111             const opensaml::saml2::NameID* nameid=nullptr,
112             const XMLCh* authncontext_class=nullptr,
113             const XMLCh* authncontext_decl=nullptr,
114             const vector<const opensaml::Assertion*>* tokens=nullptr,
115             const vector<shibsp::Attribute*>* attributes=nullptr
116             ) const {
117             return new TransformContext(attributes);
118         }
119
120         ResolutionContext* createResolutionContext(const Application& application, const Session& session) const {
121             return new TransformContext(session);
122         }
123
124         void resolveAttributes(ResolutionContext& ctx) const;
125
126         void getAttributeIds(vector<string>& attributes) const {
127             for (vector<regex_t>::const_iterator r = m_regex.begin(); r != m_regex.end(); ++r) {
128                 if (!r->get<0>().empty())
129                     attributes.push_back(r->get<0>());
130             }
131         }
132
133     private:
134         Category& m_log;
135         string m_source;
136         // dest id, regex to apply, replacement string
137         typedef tuple<string,boost::shared_ptr<RegularExpression>,const XMLCh*> regex_t;
138         vector<regex_t> m_regex;
139     };
140
141     static const XMLCh dest[] =             UNICODE_LITERAL_4(d,e,s,t);
142     static const XMLCh match[] =            UNICODE_LITERAL_5(m,a,t,c,h);
143     static const XMLCh caseSensitive[] =    UNICODE_LITERAL_13(c,a,s,e,S,e,n,s,i,t,i,v,e);
144     static const XMLCh source[] =           UNICODE_LITERAL_6(s,o,u,r,c,e);
145     static const XMLCh Regex[] =            UNICODE_LITERAL_5(R,e,g,e,x);
146
147     AttributeResolver* SHIBSP_DLLLOCAL TransformAttributeResolverFactory(const DOMElement* const & e)
148     {
149         return new TransformAttributeResolver(e);
150     }
151
152 };
153
154 vector<opensaml::Assertion*> TransformContext::m_assertions;
155
156 TransformAttributeResolver::TransformAttributeResolver(const DOMElement* e)
157     : m_log(Category::getInstance(SHIBSP_LOGCAT".AttributeResolver.Transform")),
158         m_source(XMLHelper::getAttrString(e, nullptr, source))
159 {
160     if (m_source.empty())
161         throw ConfigurationException("Transform AttributeResolver requires source attribute.");
162
163     e = XMLHelper::getFirstChildElement(e, Regex);
164     while (e) {
165         if (e->hasChildNodes() && e->hasAttributeNS(nullptr, match)) {
166             const XMLCh* repl(e->getTextContent());
167             string destId(XMLHelper::getAttrString(e, nullptr, dest));
168             bool caseflag(XMLHelper::getAttrBool(e, true, caseSensitive));
169             if (repl && *repl) {
170                 try {
171                     static XMLCh options[] = { chLatin_i, chNull };
172                     boost::shared_ptr<RegularExpression> re(new RegularExpression(e->getAttributeNS(nullptr, match), (caseflag ? &chNull : options)));
173                     m_regex.push_back(make_tuple(destId, re, repl));
174                 }
175                 catch (XMLException& ex) {
176                     auto_ptr_char msg(ex.getMessage());
177                     auto_ptr_char m(e->getAttributeNS(nullptr, match));
178                     m_log.error("exception parsing regular expression (%s): %s", m.get(), msg.get());
179                 }
180             }
181         }
182         e = XMLHelper::getNextSiblingElement(e, Regex);
183     }
184
185     if (m_regex.empty())
186         throw ConfigurationException("Transform AttributeResolver requires at least one Regex element.");
187 }
188
189
190 void TransformAttributeResolver::resolveAttributes(ResolutionContext& ctx) const
191 {
192     TransformContext& tctx = dynamic_cast<TransformContext&>(ctx);
193     if (!tctx.getInputAttributes())
194         return;
195
196     for (vector<Attribute*>::const_iterator a = tctx.getInputAttributes()->begin(); a != tctx.getInputAttributes()->end(); ++a) {
197         if (m_source != (*a)->getId() || (*a)->valueCount() == 0) {
198             continue;
199         }
200
201         // We run each transform expression against each value of the input. Each transform either generates
202         // a new attribute from its dest property, or overwrites a SimpleAttribute's values in place.
203
204         for (vector<regex_t>::const_iterator r = m_regex.begin(); r != m_regex.end(); ++r) {
205             SimpleAttribute* dest = nullptr;
206             auto_ptr<SimpleAttribute> destwrapper;
207
208             // First tuple element is the destination attribute ID, if any.
209             if (r->get<0>().empty()) {
210                 // Can we transform in-place?
211                 dest = dynamic_cast<SimpleAttribute*>(*a);
212                 if (!dest) {
213                     m_log.warn("can't transform non-simple attribute (%s) 'in place'", m_source.c_str());
214                     continue;
215                 }
216             }
217             else {
218                 // Create a destination attribute.
219                 vector<string> ids(1, r->get<0>());
220                 destwrapper.reset(new SimpleAttribute(ids));
221             }
222
223             if (dest)
224                 m_log.debug("applying in-place transform to source attribute (%s)", m_source.c_str());
225             else
226                 m_log.debug("applying transform from source attribute (%s) to dest attribute (%s)", m_source.c_str(), r->get<0>().c_str());
227
228             for (size_t i = 0; i < (*a)->valueCount(); ++i) {
229                 auto_arrayptr<XMLCh> srcval(fromUTF8((*a)->getSerializedValues()[i].c_str()));
230                 try {
231                     XMLCh* destval = r->get<1>()->replace(srcval.get(), r->get<2>());
232                     if (destval) {
233                         auto_arrayptr<char> narrow(toUTF8(destval));
234                         XMLString::release(&destval);
235                         if (dest) {
236                             // Modify in place.
237                             dest->getValues()[i] = narrow.get();
238                         }
239                         else {
240                             // Add to new object.
241                             destwrapper->getValues().push_back(narrow.get());
242                         }
243                     }
244                 }
245                 catch (XMLException& ex) {
246                     auto_ptr_char msg(ex.getMessage());
247                     m_log.error("caught error applying regular expression: %s", msg.get());
248                 }
249             }
250
251             // Save off new object.
252             if (destwrapper.get()) {
253                 ctx.getResolvedAttributes().push_back(destwrapper.get());
254                 destwrapper.release();
255             }
256         }
257     }
258 }