/** * Filter outgoing identities by name and or type. Name filter is specified using regular * expressions * * @author K. Benedyczak */ public class FilterIdentityAction extends AbstractOutputTranslationAction { private static final Logger log = Log.getLogger(Log.U_SERVER_TRANSLATION, FilterIdentityAction.class); private String identity; private Pattern idValueRegexp; public FilterIdentityAction(String[] params, TranslationActionDescription desc) throws EngineException { super(desc, params); setParameters(params); } @Override protected void invokeWrapped( TranslationInput input, Object mvelCtx, String currentProfile, TranslationResult result) throws EngineException { Set<IdentityParam> copy = new HashSet<IdentityParam>(result.getIdentities()); for (IdentityParam i : copy) { if ((identity == null || i.getTypeId().equals(identity)) && (idValueRegexp == null || idValueRegexp.matcher(i.getValue()).matches())) { log.debug("Filtering the identity " + i.toString()); result.getIdentities().remove(i); } } } private void setParameters(String[] parameters) { if (parameters.length != 2) throw new IllegalArgumentException("Action requires exactly 2 parameters"); identity = parameters[0]; idValueRegexp = parameters[1] == null ? null : Pattern.compile(parameters[1]); } }
/** * Email notification facility. * * @author K. Benedyczak */ @Component public class EmailFacility implements NotificationFacility { private static final Logger log = Log.getLogger(Log.U_SERVER, EmailFacility.class); public static final String NAME = "email"; private static final String CFG_USER = "******"; private static final String CFG_PASSWD = "mailx.smtp.auth.password"; private static final String CFG_TRUST_ALL = "mailx.smtp.trustAll"; private ExecutorsService executorsService; private PKIManagement pkiManagement; private AttributesHelper attributesHelper; private DBIdentities dbIdentities; private IdentitiesResolver idResolver; @Autowired public EmailFacility( ExecutorsService executorsService, PKIManagement pkiManagement, AttributesHelper attributeHelper, IdentitiesResolver idResolver, DBIdentities dbIdentities) { this.executorsService = executorsService; this.pkiManagement = pkiManagement; this.attributesHelper = attributeHelper; this.idResolver = idResolver; this.dbIdentities = dbIdentities; } @Override public String getName() { return NAME; } @Override public String getDescription() { return "Sends notifications by e-mail"; } @Override public void validateConfiguration(String configuration) throws WrongArgumentException { // TODO create properties helper and validate more throughly? Properties props = new Properties(); try { props.load(new StringReader(configuration)); } catch (IOException e) { throw new WrongArgumentException( "Email configuration is invalid: " + "not a valid properties syntax was used", e); } } @Override public NotificationChannelInstance getChannel(String configuration) { return new EmailChannel(configuration); } /** * Address is established as follows (first found is returned): * * <ol> * <li>entity's identity of email type, confirmed * <li>entity's attribute selected as contact email, confirmed * <li>entity's identity of email type * <li>entity's attribute selected as contact email * </ol> * * In each case if there are more then one addresses the first in the list is returned. */ @Override public String getAddressForEntity(EntityParam recipient, SqlSession sql, String preferredAddress) throws EngineException { List<VerifiableEmail> emailIds = getEmailIdentities(recipient, sql); AttributeExt<?> emailAttr = attributesHelper.getAttributeByMetadata( recipient, "/", ContactEmailMetadataProvider.NAME, sql); if (preferredAddress != null && isPresent(preferredAddress, emailIds, emailAttr)) return preferredAddress; String confirmedOnly = getAddressFrom(emailIds, emailAttr, true); if (confirmedOnly != null) return confirmedOnly; String plain = getAddressFrom(emailIds, emailAttr, false); if (plain != null) return plain; throw new IllegalIdentityValueException("The entity does not have the email address specified"); } private boolean isPresent( String address, List<VerifiableEmail> emailIds, AttributeExt<?> emailAttr) { for (VerifiableEmail ve : emailIds) if (ve.getValue().equals(address)) return true; if (emailAttr != null) { for (Object emailO : emailAttr.getValues()) { if (emailO.toString().equals(address)) return true; } } return false; } /** * Address is established as in {@link #getAddressForEntity(EntityParam, SqlSession)} however only * the input from the registration request is used and the cases with "confirmed" status are * skipped. */ @Override public String getAddressForRegistrationRequest( RegistrationRequestState currentRequest, SqlSession sql) throws EngineException { List<VerifiableEmail> emailIds = getEmailIdentities(currentRequest); Attribute<?> emailAttr = getEmailAttributeFromRequest(currentRequest, sql); return getAddressFrom(emailIds, emailAttr, false); } private String getAddressFrom( List<VerifiableEmail> emailIds, Attribute<?> emailAttr, boolean useConfirmed) { for (VerifiableEmail id : emailIds) if (!useConfirmed || id.isConfirmed()) return id.getValue(); if (emailAttr != null && (!useConfirmed || emailAttr.getAttributeSyntax().isVerifiable())) for (Object emailO : emailAttr.getValues()) { if (!useConfirmed) { return emailO.toString(); } else if (emailAttr.getAttributeSyntax().isVerifiable()) { VerifiableEmail email = (VerifiableEmail) emailO; if (!useConfirmed || email.isConfirmed()) return email.getValue(); } } return null; } private List<VerifiableEmail> getEmailIdentities(EntityParam recipient, SqlSession sql) throws EngineException { List<VerifiableEmail> emailIds = new ArrayList<>(); long entityId = idResolver.getEntityId(recipient, sql); Identity[] identities = dbIdentities.getIdentitiesForEntityNoContext(entityId, sql); for (Identity id : identities) if (id.getTypeId().equals(EmailIdentity.ID)) emailIds.add(EmailIdentity.fromIdentityParam(id)); return emailIds; } private List<VerifiableEmail> getEmailIdentities(RegistrationRequestState currentRequest) throws EngineException { List<VerifiableEmail> emailIds = new ArrayList<>(); List<IdentityParam> identities = currentRequest.getRequest().getIdentities(); if (identities == null) return emailIds; for (IdentityParam id : identities) if (id != null && id.getTypeId().equals(EmailIdentity.ID)) emailIds.add(EmailIdentity.fromIdentityParam(id)); return emailIds; } private Attribute<?> getEmailAttributeFromRequest( RegistrationRequestState currentRequest, SqlSession sql) throws EngineException { List<Attribute<?>> attrs = currentRequest.getRequest().getAttributes(); if (attrs == null) return null; AttributeType at = attributesHelper.getAttributeTypeWithSingeltonMetadata( ContactEmailMetadataProvider.NAME, sql); if (at == null) return null; for (Attribute<?> ap : attrs) { if (ap == null) continue; if (ap.getName().equals(at.getName()) && ap.getGroupPath().equals("/")) return ap; } return null; } private class EmailChannel implements NotificationChannelInstance { private Session session; public EmailChannel(String configuration) { Properties props = new Properties(); try { props.load(new StringReader(configuration)); } catch (IOException e) { // really shouldn't happen throw new IllegalStateException( "Bug: can't load email properties " + "for the channel instance", e); } String smtpUser = props.getProperty(CFG_USER); String smtpPassword = props.getProperty(CFG_PASSWD); Authenticator smtpAuthn = (smtpUser != null && smtpPassword != null) ? new SimpleAuthenticator(smtpUser, smtpPassword) : null; String trustAll = props.getProperty(CFG_TRUST_ALL); if (trustAll != null && "true".equalsIgnoreCase(trustAll)) { MailSSLSocketFactory trustAllSF; try { trustAllSF = new MailSSLSocketFactory(); } catch (GeneralSecurityException e) { // really shouldn't happen throw new IllegalStateException("Can't init trust-all SSL socket factory", e); } trustAllSF.setTrustAllHosts(true); props.put("mail.smtp.ssl.socketFactory", trustAllSF); } else { X509CertChainValidator validator = pkiManagement.getMainAuthnAndTrust().getValidator(); SSLSocketFactory factory = SocketFactoryCreator.getSocketFactory(null, validator); props.put("mail.smtp.ssl.socketFactory", factory); } session = Session.getInstance(props, smtpAuthn); } @Override public Future<NotificationStatus> sendNotification( final String recipientAddress, final String msgSubject, final String message) { final NotificationStatus retStatus = new NotificationStatus(); return executorsService .getService() .submit( new Runnable() { @Override public void run() { try { sendEmail(msgSubject, message, recipientAddress); } catch (Exception e) { log.error("E-mail notification failed", e); retStatus.setProblem(e); } } }, retStatus); } private void sendEmail(String subject, String body, String to) throws MessagingException { log.debug("Sending e-mail message to '" + to + "' with subject: " + subject); MimeMessage msg = new MimeMessage(session); msg.setFrom(); msg.setRecipients(Message.RecipientType.TO, to); msg.setSubject(subject); msg.setSentDate(new Date()); msg.setText(body); Transport.send(msg); } @Override public String getFacilityId() { return NAME; } } private static class SimpleAuthenticator extends Authenticator { private String user; private String password; public SimpleAuthenticator(String user, String password) { this.user = user; this.password = password; } protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(user, password); } } }
/** * Implementation of {@link SessionManagement} * * @author K. Benedyczak */ @Component public class SessionManagementImpl implements SessionManagement { private static final Logger log = Log.getLogger(Log.U_SERVER, SessionManagementImpl.class); public static final long DB_ACTIVITY_WRITE_DELAY = 3000; public static final String SESSION_TOKEN_TYPE = "session"; private TokensManagement tokensManagement; private LoginToHttpSessionBinder sessionBinder; private SessionParticipantTypesRegistry participantTypesRegistry; private DBIdentities dbIdentities; private DBSessionManager db; /** map of timestamps indexed by session ids, when the last activity update was written to DB. */ private Map<String, Long> recentUsageUpdates = new WeakHashMap<>(); @Autowired public SessionManagementImpl( TokensManagement tokensManagement, ExecutorsService execService, LoginToHttpSessionBinder sessionBinder, SessionParticipantTypesRegistry participantTypesRegistry, DBIdentities dbIdentities, DBSessionManager db) { this.tokensManagement = tokensManagement; this.sessionBinder = sessionBinder; this.participantTypesRegistry = participantTypesRegistry; this.dbIdentities = dbIdentities; this.db = db; execService .getService() .scheduleWithFixedDelay(new TerminateInactiveSessions(), 20, 30, TimeUnit.SECONDS); } @Override public LoginSession getCreateSession( long loggedEntity, AuthenticationRealm realm, String entityLabel, boolean outdatedCredential, Date absoluteExpiration) { Object transaction = tokensManagement.startTokenTransaction(); try { try { LoginSession ret = getOwnedSession(new EntityParam(loggedEntity), realm.getName(), transaction); if (ret != null) { ret.setLastUsed(new Date()); byte[] contents = ret.getTokenContents(); tokensManagement.updateToken( SESSION_TOKEN_TYPE, ret.getId(), null, contents, transaction); if (log.isDebugEnabled()) log.debug( "Using existing session " + ret.getId() + " for logged entity " + ret.getEntityId() + " in realm " + realm.getName()); tokensManagement.commitTokenTransaction(transaction); return ret; } } catch (EngineException e) { throw new InternalException( "Can't retrieve current sessions of the " + "authenticated user", e); } LoginSession ret = createSession( loggedEntity, realm, entityLabel, outdatedCredential, absoluteExpiration, transaction); tokensManagement.commitTokenTransaction(transaction); if (log.isDebugEnabled()) log.debug( "Created a new session " + ret.getId() + " for logged entity " + ret.getEntityId() + " in realm " + realm.getName()); return ret; } finally { tokensManagement.closeTokenTransaction(transaction); cleanScheduledRemoval(loggedEntity); } } private void cleanScheduledRemoval(long loggedEntity) { SqlSession sqlMap = db.getSqlSession(true); try { dbIdentities.clearScheduledRemovalStatus(loggedEntity, sqlMap); sqlMap.commit(); } catch (EngineException e) { log.error("Can not clear automatic removal (if any) from the user being logged in", e); } finally { db.releaseSqlSession(sqlMap); } } private LoginSession createSession( long loggedEntity, AuthenticationRealm realm, String entityLabel, boolean outdatedCredential, Date absoluteExpiration, Object transaction) { UUID randomid = UUID.randomUUID(); String id = randomid.toString(); LoginSession ls = new LoginSession( id, new Date(), absoluteExpiration, realm.getMaxInactivity() * 1000, loggedEntity, realm.getName()); ls.setUsedOutdatedCredential(outdatedCredential); ls.setEntityLabel(entityLabel); try { tokensManagement.addToken( SESSION_TOKEN_TYPE, id, new EntityParam(loggedEntity), ls.getTokenContents(), ls.getStarted(), ls.getExpires(), transaction); } catch (Exception e) { throw new InternalException("Can't create a new session", e); } return ls; } @Override public void updateSessionAttributes(String id, AttributeUpdater updater) throws WrongArgumentException { Object transaction = tokensManagement.startTokenTransaction(); try { Token token = tokensManagement.getTokenById(SESSION_TOKEN_TYPE, id, transaction); LoginSession session = token2session(token); updater.updateAttributes(session.getSessionData()); byte[] contents = session.getTokenContents(); tokensManagement.updateToken(SESSION_TOKEN_TYPE, id, null, contents, transaction); tokensManagement.commitTokenTransaction(transaction); } finally { tokensManagement.closeTokenTransaction(transaction); } } @Override public void removeSession(String id, boolean soft) { sessionBinder.removeLoginSession(id, soft); try { tokensManagement.removeToken(SESSION_TOKEN_TYPE, id, null); if (log.isDebugEnabled()) log.debug("Removed session with id " + id); } catch (WrongArgumentException e) { // not found - ok } } @Override public LoginSession getSession(String id) throws WrongArgumentException { Token token = tokensManagement.getTokenById(SESSION_TOKEN_TYPE, id, null); return token2session(token); } private LoginSession getOwnedSession(EntityParam owner, String realm, Object transaction) throws EngineException { List<Token> tokens = tokensManagement.getOwnedTokens(SESSION_TOKEN_TYPE, owner, transaction); for (Token token : tokens) { LoginSession ls = token2session(token); if (realm.equals(ls.getRealm())) return ls; } return null; } @Override public LoginSession getOwnedSession(EntityParam owner, String realm) throws EngineException { LoginSession ret = getOwnedSession(owner, realm, null); if (ret == null) throw new WrongArgumentException("No session for this owner in the given realm"); return ret; } @Override public void updateSessionActivity(String id) throws WrongArgumentException { Long lastWrite = recentUsageUpdates.get(id); if (lastWrite != null) { if (System.currentTimeMillis() < lastWrite + DB_ACTIVITY_WRITE_DELAY) return; } Object transaction = tokensManagement.startTokenTransaction(); try { Token token = tokensManagement.getTokenById(SESSION_TOKEN_TYPE, id, transaction); LoginSession session = token2session(token); session.setLastUsed(new Date()); byte[] contents = session.getTokenContents(); tokensManagement.updateToken(SESSION_TOKEN_TYPE, id, null, contents, transaction); tokensManagement.commitTokenTransaction(transaction); log.trace("Updated in db session activity timestamp for " + id); recentUsageUpdates.put(id, System.currentTimeMillis()); } finally { tokensManagement.closeTokenTransaction(transaction); } } @Override public void addSessionParticipant(SessionParticipant... participant) { InvocationContext invocationContext = InvocationContext.getCurrent(); LoginSession ls = invocationContext.getLoginSession(); try { SessionParticipants.AddParticipantToSessionTask addTask = new SessionParticipants.AddParticipantToSessionTask( participantTypesRegistry, participant); updateSessionAttributes(ls.getId(), addTask); } catch (WrongArgumentException e) { throw new InternalException("Can not add session participant to the existing session?", e); } } private LoginSession token2session(Token token) { LoginSession session = new LoginSession(); session.deserialize(token); return session; } private class TerminateInactiveSessions implements Runnable { @Override public void run() { List<Token> tokens = tokensManagement.getAllTokens(SESSION_TOKEN_TYPE); long now = System.currentTimeMillis(); for (Token t : tokens) { if (t.getExpires() != null) continue; LoginSession session = token2session(t); long inactiveFor = now - session.getLastUsed().getTime(); if (inactiveFor > session.getMaxInactivity()) { log.debug("Expiring login session " + session + " inactive for: " + inactiveFor); try { removeSession(session.getId(), false); } catch (Exception e) { log.error("Can't expire the session " + session, e); } } } } } }
/** * Configuration of OAuth client for custom provider. * * @author K. Benedyczak */ public class CustomProviderProperties extends UnityPropertiesHelper { private static final Logger log = Log.getLogger(Log.U_SERVER_CFG, CustomProviderProperties.class); public enum AccessTokenFormat { standard, httpParams }; @DocumentationReferencePrefix public static final String P = "unity.oauth2.client.CLIENT_ID."; public static final String PROVIDER_TYPE = "type"; public static final String PROVIDER_LOCATION = "authEndpoint"; public static final String ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint"; public static final String PROFILE_ENDPOINT = "profileEndpoint"; public static final String PROVIDER_NAME = "name"; public static final String CLIENT_ID = "clientId"; public static final String CLIENT_SECRET = "clientSecret"; public static final String CLIENT_AUTHN_MODE = "clientAuthenticationMode"; public static final String SCOPES = "scopes"; public static final String ACCESS_TOKEN_FORMAT = "accessTokenFormat"; public static final String OPENID_CONNECT = "openIdConnect"; public static final String OPENID_DISCOVERY = "openIdConnectDiscoveryEndpoint"; public static final String ICON_URL = "iconUrl"; public static final String CLIENT_TRUSTSTORE = "httpClientTruststore"; public static final String CLIENT_HOSTNAME_CHECKING = "httpClientHostnameChecking"; @DocumentationReferenceMeta public static final Map<String, PropertyMD> META = new HashMap<String, PropertyMD>(); static { META.put( PROVIDER_TYPE, new PropertyMD(Providers.custom) .setDescription( "Type of provider. Either a well known provider type can be specified" + " or 'custom'. In the first case only few additional settings are required: " + "client id, secret and translation profile. Other settings as scope " + "can be additionally set to fine tune the remote authentication. " + "In the latter 'custom' case all mandatory options must be set.")); META.put( PROVIDER_LOCATION, new PropertyMD() .setDescription( "Location (URL) of OAuth2 provider's authorization endpoint. " + "It is mandatory for non OpenID Connect providers, in whose case " + "the endopint can be discovered.")); META.put( ACCESS_TOKEN_ENDPOINT, new PropertyMD() .setDescription( "Location (URL) of OAuth2 provider's access token endpoint. " + "In case of OpenID Connect mode can be discovered, otherwise mandatory.")); META.put( PROFILE_ENDPOINT, new PropertyMD() .setDescription( "Location (URL) of OAuth2 provider's user's profile endpoint. " + "It is used to obtain additional user's attributes. " + "It can be autodiscovered for OpenID Connect mode. Otherwise it must be" + " set as otherwise there is no information about the user identity.")); META.put( PROVIDER_NAME, new PropertyMD() .setMandatory() .setCanHaveSubkeys() .setDescription( "Name of the OAuth provider to be displayed. Can be localized with locale subkeys.")); META.put( ICON_URL, new PropertyMD() .setCanHaveSubkeys() .setDescription( "URL to provider's logo. Can be http(s), file or data scheme. Can be localized.")); META.put( CLIENT_ID, new PropertyMD() .setMandatory() .setDescription( "Client identifier, obtained during Unity's " + "registration at the provider")); META.put( CLIENT_SECRET, new PropertyMD() .setSecret() .setMandatory() .setDescription( "Client secret, obtained during Unity's " + "registration at the provider")); META.put( CLIENT_AUTHN_MODE, new PropertyMD(ClientAuthnMode.secretBasic) .setDescription( "Defines how the client secret and id should be passed to the provider.")); META.put( SCOPES, new PropertyMD() .setDescription( "Space separated list of authorization scopes to be requested. " + "Most often required if in non OpenID Connect mode, otherwise has a default " + "value of 'openid email'")); META.put( ACCESS_TOKEN_FORMAT, new PropertyMD(AccessTokenFormat.standard) .setDescription( "Some providers (Facebook) use legacy format of a response to " + "the access token query. Non standard format can be set here.")); META.put( OPENID_CONNECT, new PropertyMD("false") .setDescription( "If set to true, then the provider is treated as OpenID " + "Connect 1.0 provider. For such providers specifying " + PROFILE_ENDPOINT + " is not mandatory as the basic user information " + "is retrieved together with access token. However the " + "discovery endpoint must be set.")); META.put( OPENID_DISCOVERY, new PropertyMD() .setDescription( "OpenID Connect Discovery endpoint address, relevant (and required) " + "only when OpenID Connect mode is turned on.")); META.put( CommonWebAuthnProperties.REGISTRATION_FORM, new PropertyMD() .setDescription( "Registration form to be shown for the locally unknown users which " + "were successfuly authenticated remotely.")); META.put( CommonWebAuthnProperties.TRANSLATION_PROFILE, new PropertyMD() .setMandatory() .setDescription( "Translation profile which will be used to map received user " + "information to a local representation.")); META.put( CommonWebAuthnProperties.ENABLE_ASSOCIATION, new PropertyMD("true") .setDescription( "If true then unknown remote user gets an option to associate " + "the remote identity with an another local (already existing) account.")); META.put( CLIENT_HOSTNAME_CHECKING, new PropertyMD(ServerHostnameCheckingMode.FAIL) .setDescription( "Controls how to react on the DNS name mismatch with " + "the server's certificate. Unless in testing environment " + "should be left on the default setting.")); META.put( CLIENT_TRUSTSTORE, new PropertyMD() .setDescription( "Name of the truststore which should be used" + " to validate TLS peer's certificates. " + "If undefined then the system Java tuststore is used.")); } private X509CertChainValidator validator = null; public CustomProviderProperties(Properties properties, String prefix, PKIManagement pkiManagement) throws ConfigurationException { super(prefix, properties, META, log); boolean openIdConnect = getBooleanValue(OPENID_CONNECT); if (openIdConnect) { if (!isSet(SCOPES)) setProperty(SCOPES, "openid email"); if (!isSet(OPENID_DISCOVERY)) throw new ConfigurationException( getKeyDescription(OPENID_DISCOVERY) + " is mandatory in OpenID Connect mode"); } else { if (!isSet(PROVIDER_LOCATION)) throw new ConfigurationException( getKeyDescription(PROVIDER_LOCATION) + " is mandatory in non OpenID Connect mode"); if (!isSet(ACCESS_TOKEN_ENDPOINT)) throw new ConfigurationException( getKeyDescription(ACCESS_TOKEN_ENDPOINT) + " is mandatory in non OpenID Connect mode"); if (!isSet(PROFILE_ENDPOINT)) throw new ConfigurationException( getKeyDescription(PROFILE_ENDPOINT) + " is mandatory in non OpenID Connect mode"); } if (!isSet(PROVIDER_NAME)) throw new ConfigurationException(getKeyDescription(PROVIDER_NAME) + " is mandatory"); String validatorName = getValue(CLIENT_TRUSTSTORE); if (validatorName != null) { try { if (!pkiManagement.getValidatorNames().contains(validatorName)) throw new ConfigurationException( "The validator " + validatorName + " for the OAuth verification client does not exist"); validator = pkiManagement.getValidator(validatorName); } catch (EngineException e) { throw new ConfigurationException( "Can not establish the validator " + validatorName + " for the OAuth verification client", e); } } } public Properties getProperties() { return properties; } public X509CertChainValidator getValidator() { return validator; } }