diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..84ac697a --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * 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 dialect governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..2cc7d4a5 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/mvnw b/mvnw new file mode 100644 index 00000000..41c0f0c2 --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..f0081db3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index 4d5a6463..047b723e 100644 --- a/pom.xml +++ b/pom.xml @@ -31,10 +31,17 @@ http://github.com/vlingo/xoom-auth/tree/master + 1.8 + ${jdk.version} + ${jdk.version} UTF-8 - 1.8 - 1.8 + UTF-8 + io.vlingo.xoom.auth.infrastructure.XoomInitializer + 2.22.2 + 2.22.2 + 1.9.1-SNAPSHOT 21.1.0 + 5.8.1 @@ -75,36 +82,322 @@ + + io.vlingo.xoom + xoom-build-plugins + ${vlingo.xoom.version} + + + + push + + + + + push-schema + + + ${basedir}/src/main/vlingo/schemata + + http://xoom-schemata:9019 + vlingo + xoom + true + true + + + + vlingo:xoom:auth:Credential:1.0.0 + Credential.vss + + + vlingo:xoom:auth:PersonName:1.0.0 + PersonName.vss + + + vlingo:xoom:auth:Constraint:1.0.0 + Constraint.vss + + + vlingo:xoom:auth:Profile:1.0.0 + Profile.vss + + + vlingo:xoom:auth:RoleProvisioned:1.0.0 + RoleProvisioned.vss + + + vlingo:xoom:auth:RoleDescriptionChanged:1.0.0 + RoleDescriptionChanged.vss + + + vlingo:xoom:auth:GroupAssignedToRole:1.0.0 + GroupAssignedToRole.vss + + + vlingo:xoom:auth:GroupUnassignedFromRole:1.0.0 + GroupUnassignedFromRole.vss + + + vlingo:xoom:auth:UserUnassignedFromRole:1.0.0 + UserUnassignedFromRole.vss + + + vlingo:xoom:auth:UserAssignedToRole:1.0.0 + UserAssignedToRole.vss + + + vlingo:xoom:auth:RolePermissionAttached:1.0.0 + RolePermissionAttached.vss + + + vlingo:xoom:auth:RolePermissionDetached:1.0.0 + RolePermissionDetached.vss + + + vlingo:xoom:auth:TenantActivated:1.0.0 + TenantActivated.vss + + + vlingo:xoom:auth:TenantDeactivated:1.0.0 + TenantDeactivated.vss + + + vlingo:xoom:auth:TenantDescriptionChanged:1.0.0 + TenantDescriptionChanged.vss + + + vlingo:xoom:auth:TenantNameChanged:1.0.0 + TenantNameChanged.vss + + + vlingo:xoom:auth:TenantSubscribed:1.0.0 + TenantSubscribed.vss + + + vlingo:xoom:auth:GroupProvisioned:1.0.0 + GroupProvisioned.vss + + + vlingo:xoom:auth:GroupDescriptionChanged:1.0.0 + GroupDescriptionChanged.vss + + + vlingo:xoom:auth:GroupAssignedToGroup:1.0.0 + GroupAssignedToGroup.vss + + + vlingo:xoom:auth:GroupUnassignedFromGroup:1.0.0 + GroupUnassignedFromGroup.vss + + + vlingo:xoom:auth:UserAssignedToGroup:1.0.0 + UserAssignedToGroup.vss + + + vlingo:xoom:auth:UserUnassignedFromGroup:1.0.0 + UserUnassignedFromGroup.vss + + + vlingo:xoom:auth:UserRegistered:1.0.0 + UserRegistered.vss + + + vlingo:xoom:auth:UserActivated:1.0.0 + UserActivated.vss + + + vlingo:xoom:auth:UserDeactivated:1.0.0 + UserDeactivated.vss + + + vlingo:xoom:auth:UserCredentialAdded:1.0.0 + UserCredentialAdded.vss + + + vlingo:xoom:auth:UserCredentialRemoved:1.0.0 + UserCredentialRemoved.vss + + + vlingo:xoom:auth:UserCredentialReplaced:1.0.0 + UserCredentialReplaced.vss + + + vlingo:xoom:auth:UserProfileReplaced:1.0.0 + UserProfileReplaced.vss + + + vlingo:xoom:auth:PermissionProvisioned:1.0.0 + PermissionProvisioned.vss + + + vlingo:xoom:auth:PermissionConstraintEnforced:1.0.0 + PermissionConstraintEnforced.vss + + + vlingo:xoom:auth:PermissionConstraintReplacementEnforced:1.0.0 + PermissionConstraintReplacementEnforced.vss + + + vlingo:xoom:auth:PermissionConstraintForgotten:1.0.0 + PermissionConstraintForgotten.vss + + + vlingo:xoom:auth:PermissionDescriptionChanged:1.0.0 + PermissionDescriptionChanged.vss + + + + + + + pull + + pull-schema + + + + http://xoom-schemata:9019 + Inform the client organization + Inform the client unit + + + + + + + + + + maven-shade-plugin + 3.1.0 + + + package + + shade + + + + + ${exec.mainClass} + + + + .SF + + + .DSA + + + .RSA + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + java + + -classpath + + -noverify + -XX:TieredStopAtLevel=1 + -Dcom.sun.management.jmxremote + ${exec.mainClass} + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + + %regex[.*] + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + + -parameters + + + + io.vlingo.xoom + xoom-turbo + ${vlingo.xoom.version} + + + + + + test-compile + + testCompile + + + + -parameters + + + + io.vlingo.xoom + xoom-turbo + ${vlingo.xoom.version} + + + + + + + + - - junit - junit - 4.13.2 - test - - - ch.qos.logback - logback-classic - 1.2.3 - test - io.vlingo.xoom - xoom-actors - 1.9.1-SNAPSHOT + xoom-turbo + ${vlingo.xoom.version} + compile io.vlingo.xoom - xoom-cluster - 1.9.1-SNAPSHOT + xoom-build-plugins + ${vlingo.xoom.version} + compile - io.vlingo.xoom - xoom-http - 1.9.1-SNAPSHOT + net.java.dev.jna + jna + 5.9.0 org.graalvm.sdk @@ -112,20 +405,48 @@ ${graalvm.version} provided + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit-jupiter.version} + test + + + io.rest-assured + rest-assured + 4.4.0 + test + ossrh-snapshots https://oss.sonatype.org/content/repositories/snapshots/ - - false - - - true - + false + true + + + ossrh-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + false + true + + diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/Bootstrap.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/Bootstrap.java new file mode 100644 index 00000000..0e2dcfea --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/Bootstrap.java @@ -0,0 +1,23 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.turbo.annotation.initializer.ResourceHandlers; + +import io.vlingo.xoom.http.resource.SinglePageApplicationConfiguration; +import io.vlingo.xoom.lattice.grid.Grid; +import io.vlingo.xoom.turbo.XoomInitializationAware; +import io.vlingo.xoom.turbo.annotation.initializer.Xoom; + +@Xoom(name = "xoom-auth") +@ResourceHandlers(packages = "io.vlingo.xoom.auth.infrastructure.resource") +public class Bootstrap implements XoomInitializationAware { + + @Override + public void onInit(final Grid grid) { + } + + @Override + public SinglePageApplicationConfiguration singlePageApplicationResource() { + return SinglePageApplicationConfiguration.defineWith("/frontend", "/app"); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/ConstraintData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/ConstraintData.java new file mode 100644 index 00000000..9dd45014 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/ConstraintData.java @@ -0,0 +1,67 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.value.Constraint; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class ConstraintData { + + public final String type; + public final String name; + public final String value; + public final String description; + + public static ConstraintData from(final String type, final String name, final String value, final String description) { + return new ConstraintData(type, name, value, description); + } + + private ConstraintData(final String type, final String name, final String value, final String description) { + this.description = description; + this.name = name; + this.type = type; + this.value = value; + } + + public Constraint toConstraint() { + return Constraint.from(Constraint.Type.valueOf(type), name, value, description); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(description) + .append(name) + .append(type) + .append(value) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ConstraintData another = (ConstraintData) other; + return new EqualsBuilder() + .append(this.description, another.description) + .append(this.name, another.name) + .append(this.type, another.type) + .append(this.value, another.value) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("description", description) + .append("name", name) + .append("type", type) + .append("value", value) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/CredentialData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/CredentialData.java new file mode 100644 index 00000000..930f9d86 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/CredentialData.java @@ -0,0 +1,97 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.crypto.AuthHasher; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import io.vlingo.xoom.auth.model.value.*; + +public class CredentialData { + + public final String authority; + public final String id; + public final String secret; + public final String type; + + public static CredentialData from(final Credential credential) { + if (credential == null) { + return CredentialData.empty(); + } else { + return from(credential.authority, credential.id, credential.secret, credential.type.name()); + } + } + + public static CredentialData from(final String authority, final String id, final String secret) { + return from(authority, id, secret, null); + } + + public static CredentialData from(final String authority, final String id, final String secret, final String type) { + return new CredentialData(authority, id, secret, type); + } + + public static Set fromAll(final Set correspondingObjects) { + return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(CredentialData::from).collect(Collectors.toSet()); + } + + public static List fromAll(final List correspondingObjects) { + return correspondingObjects == null ? Collections.emptyList() : correspondingObjects.stream().map(CredentialData::from).collect(Collectors.toList()); + } + + private CredentialData(final String authority, final String id, final String secret, final String type) { + this.authority = authority; + this.id = id; + this.secret = secret; + this.type = type; + } + + public Credential toCredential() { + final String cryptoSecret = AuthHasher.defaultHasher().hash(secret); + return Credential.from(authority, id, cryptoSecret, type); + } + + public static CredentialData empty() { + return new CredentialData(null, null, null, null); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(authority) + .append(id) + .append(secret) + .append(type) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + CredentialData another = (CredentialData) other; + return new EqualsBuilder() + .append(this.authority, another.authority) + .append(this.id, another.id) + .append(this.secret, another.secret) + .append(this.type, another.type) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("authority", authority) + .append("id", id) + .append("secret", secret) + .append("type", type) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/Events.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/Events.java new file mode 100644 index 00000000..58c0f16c --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/Events.java @@ -0,0 +1,35 @@ +package io.vlingo.xoom.auth.infrastructure; + +public enum Events { + RolePermissionAttached, + UserUnassignedFromRole, + UserRegistered, + TenantDeactivated, + PermissionProvisioned, + TenantDescriptionChanged, + UserAssignedToRole, + UserAssignedToGroup, + UserCredentialRemoved, + PermissionConstraintEnforced, + GroupAssignedToGroup, + TenantNameChanged, + UserCredentialReplaced, + GroupAssignedToRole, + GroupUnassignedFromRole, + GroupDescriptionChanged, + GroupUnassignedFromGroup, + RoleProvisioned, + UserUnassignedFromGroup, + GroupProvisioned, + TenantActivated, + PermissionConstraintForgotten, + UserCredentialAdded, + UserProfileReplaced, + PermissionDescriptionChanged, + UserDeactivated, + RoleDescriptionChanged, + TenantSubscribed, + UserActivated, + RolePermissionDetached, + PermissionConstraintReplacementEnforced, +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/GroupData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/GroupData.java new file mode 100644 index 00000000..81015917 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/GroupData.java @@ -0,0 +1,60 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.group.GroupState; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@SuppressWarnings("all") +public class GroupData { + public final String id; + public final String name; + public final String description; + public final String tenantId; + + public static GroupData from(final TenantId tenantId, final String name, final String description) { + return new GroupData(tenantId, name, description); + } + + private GroupData(final TenantId tenantId, final String name, final String description) { + this.id = null; + this.name = name; + this.description = description; + this.tenantId = tenantId.idString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + GroupData another = (GroupData) other; + return new EqualsBuilder() + .append(this.id, another.id) + .append(this.name, another.name) + .append(this.description, another.description) + .append(this.tenantId, another.tenantId) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("name", name) + .append("description", description) + .append("tenantId", tenantId) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/MinimalUserData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/MinimalUserData.java new file mode 100644 index 00000000..20dad788 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/MinimalUserData.java @@ -0,0 +1,37 @@ +// Copyright © 2012-2021 VLINGO LABS. All rights reserved. +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. If a copy of the MPL +// was not distributed with this file, You can obtain +// one at https://mozilla.org/MPL/2.0/. + +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.infrastructure.persistence.UserView; +import io.vlingo.xoom.auth.model.tenant.TenantId; + +public final class MinimalUserData { + public final boolean active; + public final PersonNameData name; + public final String tenantId; + public final String username; + + public static MinimalUserData from(final TenantId tenantId, final String username, final PersonNameData name, final boolean active) { + return new MinimalUserData(tenantId, username, name, active); + } + + public static MinimalUserData from(final UserView user) { + return new MinimalUserData(user.tenantId, user.username, PersonNameData.from(user.profile.name), user.active); + } + + private MinimalUserData(final TenantId tenantId, final String username, final PersonNameData name, final boolean active) { + this(tenantId.idString(), username, name, active); + } + + private MinimalUserData(final String tenantId, final String username, final PersonNameData name, final boolean active) { + this.tenantId = tenantId; + this.username = username; + this.name = name; + this.active = active; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/PermissionData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/PermissionData.java new file mode 100644 index 00000000..0fb07284 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/PermissionData.java @@ -0,0 +1,60 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings("all") +public class PermissionData { + public final String id; + public final Set constraints = new HashSet<>(); + public final String name; + public final String description; + public final String tenantId; + + public static PermissionData from(final TenantId tenantId, final Set constraints, final String name, final String description) { + return new PermissionData(tenantId, constraints, name, description); + } + + private PermissionData(final TenantId tenantId, final Set constraints, final String name, final String description) { + this.id = null; + this.constraints.addAll(constraints); + this.name = name; + this.description = description; + this.tenantId = tenantId.idString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + PermissionData another = (PermissionData) other; + return new EqualsBuilder() + .append(this.id, another.id) + .append(this.constraints, another.constraints) + .append(this.description, another.description) + .append(this.name, another.name) + .append(this.tenantId, another.tenantId) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("constraints", constraints) + .append("description", description) + .append("name", name) + .append("tenantId", tenantId) + .toString(); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/PersonNameData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/PersonNameData.java new file mode 100644 index 00000000..ce8d2438 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/PersonNameData.java @@ -0,0 +1,96 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.infrastructure.persistence.UserView; +import io.vlingo.xoom.auth.model.value.PersonName; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class PersonNameData { + + public final String given; + public final String family; + public final String second; + + public static PersonNameData from(final PersonName personName) { + if (personName == null) { + return PersonNameData.empty(); + } else { + return from(personName.given, personName.family, personName.second); + } + } + + public static PersonNameData from(final UserView.PersonNameView name) { + return new PersonNameData(name.given, name.family, name.second); + } + + public static PersonNameData from(final String given, final String family, final String second) { + return new PersonNameData(given, family, second); + } + + public static Set fromAll(final Set correspondingObjects) { + return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(PersonNameData::from).collect(Collectors.toSet()); + } + + public static List fromAll(final List correspondingObjects) { + return correspondingObjects == null ? Collections.emptyList() : correspondingObjects.stream().map(PersonNameData::from).collect(Collectors.toList()); + } + + public static PersonNameData of(String given, String second, String family) { + return from(given, family, second); + } + + private PersonNameData (final String given, final String family, final String second) { + this.given = given; + this.family = family; + this.second = second; + } + + public PersonName toPersonName() { + return PersonName.from(given, family, second); + } + + public static PersonNameData empty() { + return new PersonNameData(null, null, null); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(given) + .append(family) + .append(second) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + PersonNameData another = (PersonNameData) other; + return new EqualsBuilder() + .append(this.given, another.given) + .append(this.family, another.family) + .append(this.second, another.second) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("given", given) + .append("family", family) + .append("second", second) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/ProfileData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/ProfileData.java new file mode 100644 index 00000000..4d0fb023 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/ProfileData.java @@ -0,0 +1,92 @@ +package io.vlingo.xoom.auth.infrastructure; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import io.vlingo.xoom.auth.model.value.*; + +public class ProfileData { + + public final String emailAddress; + public final PersonNameData name; + public final String phone; + + public static ProfileData from(final Profile profile) { + if (profile == null) { + return ProfileData.empty(); + } else { + final PersonNameData name = profile.name != null ? PersonNameData.from(profile.name) : null; + return from(name, profile.emailAddress, profile.phone); + } + } + + public static ProfileData from(final PersonNameData name, final String emailAddress, final String phone) { + return new ProfileData(emailAddress, name, phone); + } + + public static ProfileData from(final String emailAddress, final PersonNameData name, final String phone) { + return new ProfileData(emailAddress, name, phone); + } + + public static Set fromAll(final Set correspondingObjects) { + return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(ProfileData::from).collect(Collectors.toSet()); + } + + public static List fromAll(final List correspondingObjects) { + return correspondingObjects == null ? Collections.emptyList() : correspondingObjects.stream().map(ProfileData::from).collect(Collectors.toList()); + } + + private ProfileData (final String emailAddress, final PersonNameData name, final String phone) { + this.emailAddress = emailAddress; + this.name = name; + this.phone = phone; + } + + public Profile toProfile() { + final PersonName name = PersonName.from(this.name.given, this.name.family, this.name.second); + return Profile.from(emailAddress, name, phone); + } + + public static ProfileData empty() { + return new ProfileData(null, null, null); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(emailAddress) + .append(name) + .append(phone) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ProfileData another = (ProfileData) other; + return new EqualsBuilder() + .append(this.emailAddress, another.emailAddress) + .append(this.name, another.name) + .append(this.phone, another.phone) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("emailAddress", emailAddress) + .append("name", name) + .append("phone", phone) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/RoleData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/RoleData.java new file mode 100644 index 00000000..15b93c98 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/RoleData.java @@ -0,0 +1,58 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.RoleState; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; +import java.util.stream.Collectors; + +@SuppressWarnings("all") +public class RoleData { + public final String id; + public final String tenantId; + public final String name; + public final String description; + + public static RoleData from(final TenantId tenantId, final String name, final String description) { + return new RoleData(tenantId, name, description); + } + + private RoleData(final TenantId tenantId, final String name, final String description) { + this.id = null; + this.tenantId = tenantId.idString(); + this.name = name; + this.description = description; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + RoleData another = (RoleData) other; + return new EqualsBuilder() + .append(this.id, another.id) + .append(this.tenantId, another.tenantId) + .append(this.name, another.name) + .append(this.description, another.description) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("tenantId", tenantId) + .append("name", name) + .append("description", description) + .toString(); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/TenantData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/TenantData.java new file mode 100644 index 00000000..04a22111 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/TenantData.java @@ -0,0 +1,79 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.stream.Collectors; +import io.vlingo.xoom.auth.model.tenant.TenantState; +import java.util.*; + +@SuppressWarnings("all") +public class TenantData { + public final String tenantId; + public final String name; + public final String description; + public final boolean active; + + public static TenantData from(final TenantState tenantState) { + return from(tenantState.tenantId, tenantState.name, tenantState.description, tenantState.active); + } + + public static TenantData from(final TenantId tenantId, final String name, final String description, final boolean active) { + return new TenantData(tenantId, name, description, active); + } + + public static TenantData from(final String name, final String description, final boolean active) { + return from(TenantId.from(UUID.randomUUID().toString()), name, description, active); + } + + public static List fromAll(final List states) { + return states.stream().map(TenantData::from).collect(Collectors.toList()); + } + + public static TenantData empty() { + return from(TenantState.identifiedBy(TenantId.from(""))); + } + + private TenantData(final TenantId tenantId, final String name, final String description, final boolean active) { + this.tenantId = tenantId.idString(); + this.name = name; + this.description = description; + this.active = active; + } + + public TenantState toTenantState() { + return new TenantState(TenantId.from(tenantId), name, description, active); + } + + public TenantData withTenantId(String tenantId) { + return from(TenantId.from(tenantId), this.name, this.description, this.active); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + TenantData another = (TenantData) other; + return new EqualsBuilder() + .append(this.tenantId, another.tenantId) + .append(this.name, another.name) + .append(this.description, another.description) + .append(this.active, another.active) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", tenantId) + .append("name", name) + .append("description", description) + .append("active", active) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/UserRegistrationData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/UserRegistrationData.java new file mode 100644 index 00000000..8a1aef74 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/UserRegistrationData.java @@ -0,0 +1,86 @@ +package io.vlingo.xoom.auth.infrastructure; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.user.UserState; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SuppressWarnings("all") +public class UserRegistrationData { + public final String id; + public final String tenantId; + public final String username; + public final boolean active; + public final Set credentials = new HashSet<>(); + public final ProfileData profile; + + public static UserRegistrationData from(final UserState userState) { + final Set credentials = userState.credentials != null ? userState.credentials.stream().map(CredentialData::from).collect(java.util.stream.Collectors.toSet()) : new HashSet<>(); + final ProfileData profile = userState.profile != null ? ProfileData.from(userState.profile) : null; + return from(userState.userId, userState.username, profile, credentials, userState.active); + } + + public static UserRegistrationData from(final UserId userId, final String username, final ProfileData profile, final Set credentials, final boolean active) { + return new UserRegistrationData(userId, username, profile, credentials, active); + } + + public static UserRegistrationData from(final UserId userId, final String username, final ProfileData profile, final CredentialData credential, final boolean active) { + return new UserRegistrationData(userId, username, profile, Stream.of(credential).collect(Collectors.toSet()), active); + } + + private UserRegistrationData(final UserId userId, final String username, final ProfileData profile, final Set credentials, final boolean active) { + this.id = userId.idString(); + this.tenantId = userId.tenantId.idString(); + this.username = username; + this.active = active; + this.credentials.addAll(credentials); + this.profile = profile; + } + + public CredentialData credentialOf(final String authority) { + for (final CredentialData credential : credentials) { + if (credential.authority.equals(authority)) { + return credential; + } + } + return null; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + UserRegistrationData another = (UserRegistrationData) other; + return new EqualsBuilder() + .append(this.id, another.id) + .append(this.tenantId, another.tenantId) + .append(this.username, another.username) + .append(this.active, another.active) + .append(this.credentials, another.credentials) + .append(this.profile, another.profile) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("tenantId", tenantId) + .append("username", username) + .append("active", active) + .append("credentials", credentials) + .append("profile", profile) + .toString(); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionActor.java new file mode 100644 index 00000000..0f8cd008 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionActor.java @@ -0,0 +1,146 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.Events; +import io.vlingo.xoom.auth.model.group.*; +import io.vlingo.xoom.auth.model.role.GroupAssignedToRole; +import io.vlingo.xoom.auth.model.role.GroupUnassignedFromRole; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.lattice.model.projection.Projectable; +import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * See + * + * StateStoreProjectionActor + * + */ +public class GroupProjectionActor extends StateStoreProjectionActor { + + private static final GroupView Empty = GroupView.empty(); + + public GroupProjectionActor() { + this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store); + } + + public GroupProjectionActor(final StateStore stateStore) { + super(stateStore); + } + + @Override + protected GroupView currentDataFor(final Projectable projectable) { + return Empty; + } + + @Override + protected GroupView merge(final GroupView previousData, final int previousVersion, final GroupView currentData, final int currentVersion) { + + if (previousVersion == currentVersion) return currentData; + + GroupView merged = previousData; + + for (final Source event : sources()) { + switch (Events.valueOf(event.typeName())) { + case GroupProvisioned: { + final GroupProvisioned typedEvent = typed(event); + merged = GroupView.from(typedEvent.groupId, typedEvent.name, typedEvent.description); + break; + } + + case GroupDescriptionChanged: { + final GroupDescriptionChanged typedEvent = typed(event); + merged = GroupView.from(typedEvent.groupId, previousData.name, typedEvent.description, previousData.groups, previousData.users, previousData.roles); + break; + } + + case GroupAssignedToGroup: { + final GroupAssignedToGroup typedEvent = typed(event); + final Set> innerGroups = concat(previousData.groups, Relation.groupWithMember(typedEvent.groupId, typedEvent.innerGroupId)); + merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, innerGroups, previousData.users, previousData.roles); + break; + } + + case GroupUnassignedFromGroup: { + final GroupUnassignedFromGroup typedEvent = typed(event); + final Set> innerGroups = filter(previousData.groups, g -> !g.equals(Relation.groupWithMember(typedEvent.groupId, typedEvent.innerGroupId))); + merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, innerGroups, previousData.users, previousData.roles); + break; + } + + case UserAssignedToGroup: { + final UserAssignedToGroup typedEvent = typed(event); + final Set> users = concat(previousData.users, Relation.userAssignedToGroup(typedEvent.userId, typedEvent.groupId)); + merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, users, previousData.roles); + break; + } + + case UserUnassignedFromGroup: { + final UserUnassignedFromGroup typedEvent = typed(event); + final Set> users = filter(previousData.users, u -> !u.equals(Relation.userAssignedToGroup(typedEvent.userId, typedEvent.groupId))); + merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, users, previousData.roles); + break; + } + + case GroupAssignedToRole: { + final GroupAssignedToRole typedEvent = typed(event); + final Set> roles = concat(previousData.roles, Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId)); + merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, previousData.users, roles); + break; + } + + case GroupUnassignedFromRole: { + final GroupUnassignedFromRole typedEvent = typed(event); + final Set> roles = filter(previousData.roles, r -> !r.equals(Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId))); + merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, previousData.users, roles); + break; + } + + default: + logger().warn("Event of type " + event.typeName() + " was not matched."); + break; + } + } + + return merged; + } + + @Override + protected String dataIdFor(final Projectable projectable) { + final Source firstEvent = sources().get(0); + switch (Events.valueOf(firstEvent.typeName())) { + case GroupAssignedToRole: + return this.typed(firstEvent).groupId.idString(); + case GroupUnassignedFromRole: + return this.typed(firstEvent).groupId.idString(); + default: + return super.dataIdFor(projectable); + } + } + + @Override + protected int currentDataVersionFor(final Projectable projectable, final GroupView previousData, final int previousVersion) { + switch (Events.valueOf(sources().get(0).typeName())) { + case GroupAssignedToRole: + case GroupUnassignedFromRole: + return previousVersion + 1; + default: + return super.currentDataVersionFor(projectable, previousData, previousVersion); + } + } + + private Set filter(final Set items, final Predicate predicate) { + return items.stream().filter(predicate).collect(Collectors.toSet()); + } + + private Set concat(final Set items, final T item) { + return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueries.java new file mode 100644 index 00000000..682e9fbc --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueries.java @@ -0,0 +1,12 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.Collection; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.common.Completes; + +@SuppressWarnings("all") +public interface GroupQueries { + Completes groupOf(GroupId groupId); + Completes> groups(); +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesActor.java new file mode 100644 index 00000000..1a024d4a --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesActor.java @@ -0,0 +1,33 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.ArrayList; +import java.util.Collection; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.query.StateStoreQueryActor; +import io.vlingo.xoom.symbio.store.state.StateStore; + +import io.vlingo.xoom.auth.infrastructure.GroupData; + +/** + * See Querying a StateStore + */ +@SuppressWarnings("all") +public class GroupQueriesActor extends StateStoreQueryActor implements GroupQueries { + + public GroupQueriesActor(StateStore store) { + super(store); + } + + @Override + public Completes groupOf(GroupId groupId) { + return queryStateFor(groupId.idString(), GroupView.class, GroupView.empty()); + } + + @Override + public Completes> groups() { + return streamAllOf(GroupView.class, new ArrayList<>()); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupView.java new file mode 100644 index 00000000..bf406b13 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupView.java @@ -0,0 +1,94 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.group.GroupState; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.user.UserId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +public class GroupView { + public final String id; + public final String tenantId; + public final String name; + public final String description; + public final Set> groups; + public final Set> users; + public final Set> roles; + + public static GroupView empty() { + return new GroupView("", "", "", "", Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); + } + + public static GroupView from(GroupState groupState) { + return from(groupState.id, groupState.name, groupState.description); + } + + public static GroupView from(final GroupId groupId, final String name, String description) { + return from(groupId, name, description, Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); + } + + public static GroupView from(final GroupId groupId, final String name, final String description, final Set> groups, final Set> users, final Set> roles) { + return new GroupView( + groupId.idString(), + groupId.tenantId.idString(), + name, + description, + groups, + users, + roles + ); + } + + private GroupView(final String id, final String tenantId, final String name, final String description, final Set> groups, final Set> users, final Set> roles) { + this.id = id; + this.tenantId = tenantId; + this.name = name; + this.description = description; + this.groups = groups; + this.users = users; + this.roles = roles; + } + + public boolean isInRole(final RoleId roleId) { + return roles.stream().filter(r -> r.right.equals(roleId)).findFirst().isPresent(); + } + + public boolean hasMember(final GroupId groupId) { + return groups.stream().filter(g -> g.right.equals(groupId)).findFirst().isPresent(); + } + + public boolean hasMember(final UserId userId) { + return users.stream().filter(u -> u.left.equals(userId)).findFirst().isPresent(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GroupView)) return false; + GroupView groupView = (GroupView) o; + return id.equals(groupView.id) && tenantId.equals(groupView.tenantId) && name.equals(groupView.name) && description.equals(groupView.description) && groups.equals(groupView.groups) && users.equals(groupView.users) && roles.equals(groupView.roles); + } + + @Override + public int hashCode() { + return Objects.hash(id, tenantId, name, description, groups, users, roles); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("tenantId", tenantId) + .append("name", name) + .append("description", description) + .append("groups", groups) + .append("users", users) + .append("roles", roles) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionActor.java new file mode 100644 index 00000000..2a8d6be4 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionActor.java @@ -0,0 +1,142 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.Events; +import io.vlingo.xoom.auth.infrastructure.persistence.PermissionView.ConstraintView; +import io.vlingo.xoom.auth.model.permission.*; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.RolePermissionAttached; +import io.vlingo.xoom.auth.model.role.RolePermissionDetached; +import io.vlingo.xoom.lattice.model.projection.Projectable; +import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * See + * + * StateStoreProjectionActor + * + */ +public class PermissionProjectionActor extends StateStoreProjectionActor { + + private static final PermissionView Empty = PermissionView.empty(); + + public PermissionProjectionActor() { + this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store); + } + + public PermissionProjectionActor(final StateStore stateStore) { + super(stateStore); + } + + @Override + protected PermissionView currentDataFor(final Projectable projectable) { + return Empty; + } + + @Override + protected PermissionView merge(final PermissionView previousData, final int previousVersion, final PermissionView currentData, final int currentVersion) { + + if (previousVersion == currentVersion) return currentData; + + PermissionView merged = previousData; + + for (final Source event : sources()) { + switch (Events.valueOf(event.typeName())) { + case PermissionProvisioned: { + final PermissionProvisioned typedEvent = typed(event); + merged = PermissionView.from(typedEvent.permissionId, new HashSet<>(), typedEvent.name, typedEvent.description, new HashSet<>()); + break; + } + + case PermissionConstraintEnforced: { + final PermissionConstraintEnforced typedEvent = typed(event); + final ConstraintView constraint = ConstraintView.from(typedEvent.constraint); + merged = PermissionView.from(typedEvent.permissionId, concat(previousData.constraints, constraint), previousData.name, previousData.description, previousData.roles); + break; + } + + case PermissionConstraintReplacementEnforced: { + final PermissionConstraintReplacementEnforced typedEvent = typed(event); + final ConstraintView constraint = ConstraintView.from(typedEvent.constraint); + final Set constraints = concat(filter(previousData.constraints, c -> !c.name.equals(typedEvent.constraintName)), constraint); + merged = PermissionView.from(typedEvent.permissionId, constraints, previousData.name, previousData.description, previousData.roles); + break; + } + + case PermissionConstraintForgotten: { + final PermissionConstraintForgotten typedEvent = typed(event); + final Set constraints = filter(previousData.constraints, c -> !c.name.equals(typedEvent.constraintName)); + merged = PermissionView.from(typedEvent.permissionId, constraints, previousData.name, previousData.description, previousData.roles); + break; + } + + case PermissionDescriptionChanged: { + final PermissionDescriptionChanged typedEvent = typed(event); + merged = PermissionView.from(typedEvent.permissionId, previousData.constraints, previousData.name, typedEvent.description, previousData.roles); + break; + } + + case RolePermissionAttached: { + final RolePermissionAttached typedEvent = typed(event); + final Set> roles = concat(previousData.roles, Relation.roleWithPermission(typedEvent.roleId, typedEvent.permissionId)); + merged = PermissionView.from(typedEvent.permissionId, previousData.constraints, previousData.name, previousData.description, roles); + break; + } + + case RolePermissionDetached: { + final RolePermissionDetached typedEvent = typed(event); + final Set> roles = filter(previousData.roles, r -> !r.equals(Relation.roleWithPermission(typedEvent.roleId, typedEvent.permissionId))); + merged = PermissionView.from(typedEvent.permissionId, previousData.constraints, previousData.name, previousData.description, roles); + break; + } + + default: + logger().warn("Event of type " + event.typeName() + " was not matched."); + break; + } + } + + return merged; + } + + @Override + protected String dataIdFor(final Projectable projectable) { + final Source firstEvent = sources().get(0); + switch (Events.valueOf(firstEvent.typeName())) { + case RolePermissionAttached: + return this.typed(firstEvent).permissionId.idString(); + case RolePermissionDetached: + return this.typed(firstEvent).permissionId.idString(); + default: + return super.dataIdFor(projectable); + } + } + + @Override + protected int currentDataVersionFor(Projectable projectable, PermissionView previousData, int previousVersion) { + final Source firstEvent = sources().get(0); + switch (Events.valueOf(firstEvent.typeName())) { + case RolePermissionAttached: + case RolePermissionDetached: + return previousVersion + 1; + default: + return super.currentDataVersionFor(projectable, previousData, previousVersion); + } + } + + private Set filter(final Set items, final Predicate predicate) { + return items.stream().filter(predicate).collect(Collectors.toSet()); + } + + private Set concat(final Set items, final T item) { + return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueries.java new file mode 100644 index 00000000..a4c1eaa9 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueries.java @@ -0,0 +1,12 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.Collection; + +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.common.Completes; + +@SuppressWarnings("all") +public interface PermissionQueries { + Completes permissionOf(PermissionId permissionId); + Completes> permissions(); +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesActor.java new file mode 100644 index 00000000..119c8927 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesActor.java @@ -0,0 +1,33 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.ArrayList; +import java.util.Collection; + +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.query.StateStoreQueryActor; +import io.vlingo.xoom.symbio.store.state.StateStore; + +import io.vlingo.xoom.auth.infrastructure.PermissionData; + +/** + * See Querying a StateStore + */ +@SuppressWarnings("all") +public class PermissionQueriesActor extends StateStoreQueryActor implements PermissionQueries { + + public PermissionQueriesActor(StateStore store) { + super(store); + } + + @Override + public Completes permissionOf(PermissionId permissionId) { + return queryStateFor(permissionId.idString(), PermissionView.class, PermissionView.empty()); + } + + @Override + public Completes> permissions() { + return streamAllOf(PermissionView.class, new ArrayList<>()); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionView.java new file mode 100644 index 00000000..0e2b3aeb --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionView.java @@ -0,0 +1,135 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.permission.PermissionState; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.value.Constraint; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class PermissionView { + public final String id; + public final String tenantId; + public final String name; + public final String description; + public final Set constraints; + public final Set> roles; + + public static PermissionView empty() { + return from(PermissionId.from(TenantId.from(""), ""), new HashSet<>(), "", "", new HashSet<>()); + } + + public static PermissionView from(final PermissionState permissionState) { + final Set constraints = permissionState.constraints != null ? permissionState.constraints.stream().map(ConstraintView::from).collect(java.util.stream.Collectors.toSet()) : new HashSet<>(); + return from(permissionState.id, constraints, permissionState.name, permissionState.description, new HashSet<>()); + } + + public static PermissionView from(final TenantId tenantId, final Set constraints, final String name, final String description, final Set> roles) { + return new PermissionView(null, tenantId.idString(), constraints, name, description, roles); + } + + public static PermissionView from(final PermissionId permissionId, final Set constraints, final String name, final String description, final Set> roles) { + return new PermissionView(permissionId.idString(), permissionId.tenantId.idString(), constraints, name, description, roles); + } + + private PermissionView(final String id, final String tenantId, final Set constraints, final String name, final String description, final Set> roles) { + this.id = id; + this.tenantId = tenantId; + this.name = name; + this.description = description; + this.constraints = constraints; + this.roles = roles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PermissionView)) return false; + PermissionView that = (PermissionView) o; + return id.equals(that.id) && tenantId.equals(that.tenantId) && name.equals(that.name) && description.equals(that.description) && constraints.equals(that.constraints) && roles.equals(that.roles); + } + + @Override + public int hashCode() { + return Objects.hash(id, tenantId, name, description, constraints, roles); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("tenantId", tenantId) + .append("constraints", constraints) + .append("description", description) + .append("name", name) + .append("roles", roles) + .toString(); + } + + public boolean isAttachedTo(final RoleId roleId) { + return roles.stream().filter(r -> r.left.equals(roleId)).findFirst().isPresent(); + } + + public boolean isAttachedTo(final UserId userId) { + return false; + } + + public static class ConstraintView { + public final String type; + public final String name; + public final String value; + public final String description; + + public static ConstraintView from(final Constraint constraint) { + if (constraint == null) { + return ConstraintView.empty(); + } else { + return from(constraint.type.name(), constraint.name, constraint.value, constraint.description); + } + } + + public static ConstraintView from(final String type, final String name, final String value, final String description) { + return new ConstraintView(type, name, value, description); + } + + public static ConstraintView empty() { + return new ConstraintView(null, null, null, null); + } + + private ConstraintView(String type, String name, String value, String description) { + this.type = type; + this.name = name; + this.value = value; + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ConstraintView)) return false; + ConstraintView that = (ConstraintView) o; + return type.equals(that.type) && name.equals(that.name) && Objects.equals(value, that.value) && Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(type, name, value, description); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("description", description) + .append("name", name) + .append("type", type) + .append("value", value) + .toString(); + } + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PersistenceSetup.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PersistenceSetup.java new file mode 100644 index 00000000..83ec6b29 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PersistenceSetup.java @@ -0,0 +1,65 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.*; +import io.vlingo.xoom.auth.model.group.*; +import io.vlingo.xoom.auth.model.permission.*; +import io.vlingo.xoom.auth.model.role.*; +import io.vlingo.xoom.auth.model.tenant.*; +import io.vlingo.xoom.auth.model.user.*; +import io.vlingo.xoom.turbo.annotation.persistence.*; +import io.vlingo.xoom.turbo.annotation.persistence.Persistence.StorageType; + +@SuppressWarnings("unused") +@Persistence(basePackage = "io.vlingo.xoom.auth", storageType = StorageType.JOURNAL, cqrs = true) +@Projections(value = { + @Projection(actor = RoleProjectionActor.class, becauseOf = {UserAssignedToRole.class, RolePermissionAttached.class, GroupUnassignedFromRole.class, RoleDescriptionChanged.class, RoleProvisioned.class, UserUnassignedFromRole.class, RolePermissionDetached.class, GroupAssignedToRole.class}), + @Projection(actor = TenantProjectionActor.class, becauseOf = {TenantSubscribed.class, TenantDeactivated.class, TenantNameChanged.class, TenantDescriptionChanged.class, TenantActivated.class}), + @Projection(actor = GroupProjectionActor.class, becauseOf = {GroupDescriptionChanged.class, UserAssignedToGroup.class, GroupAssignedToGroup.class, GroupUnassignedFromGroup.class, UserUnassignedFromGroup.class, GroupProvisioned.class, GroupAssignedToRole.class, GroupUnassignedFromRole.class}), + @Projection(actor = UserProjectionActor.class, becauseOf = {UserCredentialAdded.class, UserProfileReplaced.class, UserCredentialRemoved.class, UserDeactivated.class, UserRegistered.class, UserActivated.class, UserCredentialReplaced.class, UserAssignedToRole.class, UserUnassignedFromRole.class}), + @Projection(actor = PermissionProjectionActor.class, becauseOf = {PermissionConstraintEnforced.class, PermissionDescriptionChanged.class, PermissionProvisioned.class, PermissionConstraintReplacementEnforced.class, PermissionConstraintForgotten.class, RolePermissionAttached.class, RolePermissionDetached.class}) +}, type = ProjectionType.EVENT_BASED) +@Adapters({ + RolePermissionAttached.class, + UserUnassignedFromRole.class, + UserRegistered.class, + TenantDeactivated.class, + PermissionProvisioned.class, + TenantDescriptionChanged.class, + UserAssignedToRole.class, + UserAssignedToGroup.class, + UserCredentialRemoved.class, + PermissionConstraintEnforced.class, + GroupAssignedToGroup.class, + TenantNameChanged.class, + UserCredentialReplaced.class, + GroupAssignedToRole.class, + GroupUnassignedFromRole.class, + GroupDescriptionChanged.class, + GroupUnassignedFromGroup.class, + RoleProvisioned.class, + UserUnassignedFromGroup.class, + GroupProvisioned.class, + TenantActivated.class, + PermissionConstraintForgotten.class, + UserCredentialAdded.class, + UserProfileReplaced.class, + PermissionDescriptionChanged.class, + UserDeactivated.class, + RoleDescriptionChanged.class, + TenantSubscribed.class, + UserActivated.class, + RolePermissionDetached.class, + PermissionConstraintReplacementEnforced.class +}) +@EnableQueries({ + @QueriesEntry(protocol = RoleQueries.class, actor = RoleQueriesActor.class), + @QueriesEntry(protocol = GroupQueries.class, actor = GroupQueriesActor.class), + @QueriesEntry(protocol = TenantQueries.class, actor = TenantQueriesActor.class), + @QueriesEntry(protocol = UserQueries.class, actor = UserQueriesActor.class), + @QueriesEntry(protocol = PermissionQueries.class, actor = PermissionQueriesActor.class), +}) +@DataObjects({CredentialData.class, ConstraintData.class, UserView.class, TenantData.class, PersonNameData.class, ProfileData.class, PermissionView.class, RoleView.class, GroupView.class}) +public class PersistenceSetup { + + +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/Relation.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/Relation.java new file mode 100644 index 00000000..df741fc1 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/Relation.java @@ -0,0 +1,117 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.user.UserId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.HashMap; +import java.util.Objects; + +import static io.vlingo.xoom.auth.infrastructure.persistence.Relation.TYPE.*; + +public class Relation { + public final L left; + public final TYPE type; + public final R right; + + public static Relation groupWithMember(final GroupId parentGroup, final GroupId childGroup) { + return new Relation<>(parentGroup, GROUP_HAS_MEMBER, childGroup); + } + + public static Relation groupWithParent(final GroupId childGroup, final GroupId parentGroup) { + return new Relation<>(childGroup, GROUP_HAS_PARENT, parentGroup); + } + + public static Relation roleWithPermission(final RoleId role, final PermissionId permission) { + return new Relation<>(role, ROLE_HAS_PERMISSION, permission); + } + + public static Relation permissionAttachedToRole(final PermissionId permission, final RoleId role) { + return new Relation<>(permission, PERMISSION_IS_ATTACHED_TO_ROLE, role); + } + + public static Relation roleWithGroup(final RoleId role, final GroupId group) { + return new Relation<>(role, ROLE_WITH_GROUP, group); + } + + public static Relation groupAssignedToRole(final GroupId group, final RoleId role) { + return new Relation<>(group, GROUP_ASSIGNED_TO_ROLE, role); + } + + public static Relation userAssignedToRole(final UserId user, final RoleId role) { + return new Relation<>(user, USER_ASSIGNED_TO_ROLE, role); + } + + public static Relation roleWithUser(final RoleId role, final UserId user) { + return new Relation<>(role, ROLE_WITH_USER, user); + } + + public static Relation userAssignedToGroup(final UserId user, final GroupId group) { + return new Relation<>(user, USER_ASSIGNED_TO_GROUP, group); + } + + public static Relation groupWithMember(final GroupId group, final UserId user) { + return new Relation<>(group, GROUP_HAS_USER_MEMBER, user); + } + + enum TYPE { + GROUP_HAS_MEMBER, + GROUP_HAS_PARENT, + ROLE_HAS_PERMISSION, + PERMISSION_IS_ATTACHED_TO_ROLE, + GROUP_ASSIGNED_TO_ROLE, + ROLE_WITH_GROUP, + USER_ASSIGNED_TO_ROLE, + ROLE_WITH_USER, + USER_ASSIGNED_TO_GROUP, + GROUP_HAS_USER_MEMBER + } + + private Relation(final L left, final TYPE type, final R right) { + this.left = left; + this.type = type; + this.right = right; + } + + public Relation invert() { + final HashMap inversions = new HashMap() {{ + put(GROUP_HAS_MEMBER, GROUP_HAS_PARENT); + put(GROUP_HAS_PARENT, GROUP_HAS_MEMBER); + put(ROLE_HAS_PERMISSION, PERMISSION_IS_ATTACHED_TO_ROLE); + put(PERMISSION_IS_ATTACHED_TO_ROLE, ROLE_HAS_PERMISSION); + put(GROUP_ASSIGNED_TO_ROLE, ROLE_WITH_GROUP); + put(ROLE_WITH_GROUP, GROUP_ASSIGNED_TO_ROLE); + put(USER_ASSIGNED_TO_ROLE, ROLE_WITH_USER); + put(ROLE_WITH_USER, USER_ASSIGNED_TO_ROLE); + put(USER_ASSIGNED_TO_GROUP, GROUP_HAS_USER_MEMBER); + put(GROUP_HAS_USER_MEMBER, USER_ASSIGNED_TO_GROUP); + }}; + + return new Relation<>(right, inversions.get(type), left); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Relation)) return false; + Relation relation = (Relation) o; + return left.equals(relation.left) && type == relation.type && right.equals(relation.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, type, right); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("left", left) + .append("type", type.name()) + .append("right", right) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionActor.java new file mode 100644 index 00000000..ca9d6303 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionActor.java @@ -0,0 +1,122 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.Events; +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.role.*; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.lattice.model.projection.Projectable; +import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * See + * + * StateStoreProjectionActor + * + */ +public class RoleProjectionActor extends StateStoreProjectionActor { + + private static final RoleView Empty = RoleView.empty(); + + public RoleProjectionActor() { + this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store); + } + + public RoleProjectionActor(final StateStore stateStore) { + super(stateStore); + } + + @Override + protected RoleView currentDataFor(final Projectable projectable) { + return Empty; + } + + @Override + protected RoleView merge(final RoleView previousData, final int previousVersion, final RoleView currentData, final int currentVersion) { + + if (previousVersion == currentVersion) return currentData; + + RoleView merged = previousData; + + for (final Source event : sources()) { + switch (Events.valueOf(event.typeName())) { + case RoleProvisioned: { + final RoleProvisioned typedEvent = typed(event); + merged = RoleView.from(typedEvent.roleId, typedEvent.name, typedEvent.description); + break; + } + + case RoleDescriptionChanged: { + final RoleDescriptionChanged typedEvent = typed(event); + merged = RoleView.from(typedEvent.roleId, previousData.name, typedEvent.description, previousData.permissions, previousData.groups, previousData.users); + break; + } + + case GroupAssignedToRole: { + final GroupAssignedToRole typedEvent = typed(event); + final Set> groups = concat(previousData.groups, Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId)); + merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, groups, previousData.users); + break; + } + + case GroupUnassignedFromRole: { + final GroupUnassignedFromRole typedEvent = typed(event); + final Set> groups = filter(previousData.groups, g -> !g.equals(Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId))); + merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, groups, previousData.users); + break; + } + + case UserAssignedToRole: { + final UserAssignedToRole typedEvent = typed(event); + final Set> users = concat(previousData.users, Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId)); + merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, previousData.groups, users); + break; + } + + case UserUnassignedFromRole: { + final UserUnassignedFromRole typedEvent = typed(event); + final Set> users = filter(previousData.users, u -> !u.equals(Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId))); + merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, previousData.groups, users); + break; + } + + case RolePermissionAttached: { + final RolePermissionAttached typedEvent = typed(event); + final Set> permissions = concat(previousData.permissions, Relation.permissionAttachedToRole(typedEvent.permissionId, typedEvent.roleId)); + merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, permissions, previousData.groups, previousData.users); + break; + } + + case RolePermissionDetached: { + final RolePermissionDetached typedEvent = typed(event); + final Set> permissions = filter(previousData.permissions, p -> !p.equals(Relation.permissionAttachedToRole(typedEvent.permissionId, typedEvent.roleId))); + merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, permissions, previousData.groups, previousData.users); + break; + } + + default: + logger().warn("Event of type " + event.typeName() + " was not matched."); + break; + } + } + + return merged; + } + + + private Set filter(final Set items, final Predicate predicate) { + return items.stream().filter(predicate).collect(Collectors.toSet()); + } + + private Set concat(final Set items, final T item) { + return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueries.java new file mode 100644 index 00000000..8fcdde89 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueries.java @@ -0,0 +1,12 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.Collection; + +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.common.Completes; + +@SuppressWarnings("all") +public interface RoleQueries { + Completes roleOf(RoleId roleId); + Completes> roles(); +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesActor.java new file mode 100644 index 00000000..c1cdaa00 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesActor.java @@ -0,0 +1,33 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.ArrayList; +import java.util.Collection; + +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.query.StateStoreQueryActor; +import io.vlingo.xoom.symbio.store.state.StateStore; + +import io.vlingo.xoom.auth.infrastructure.RoleData; + +/** + * See Querying a StateStore + */ +@SuppressWarnings("all") +public class RoleQueriesActor extends StateStoreQueryActor implements RoleQueries { + + public RoleQueriesActor(StateStore store) { + super(store); + } + + @Override + public Completes roleOf(RoleId roleId) { + return queryStateFor(roleId.idString(), RoleView.class, RoleView.empty()); + } + + @Override + public Completes> roles() { + return streamAllOf(RoleView.class, new ArrayList<>()); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleView.java new file mode 100644 index 00000000..259b7626 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleView.java @@ -0,0 +1,91 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.RoleState; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class RoleView { + public final String id; + public final String tenantId; + public final String name; + public final String description; + public final Set> permissions; + public final Set> groups; + public final Set> users; + + public static RoleView empty() { + return from(RoleId.from(TenantId.from(""), ""), "", ""); + } + + public static RoleView from(final RoleState roleState) { + return from(roleState.roleId, roleState.name, roleState.description); + } + + public static RoleView from(final TenantId tenantId, final String name, final String description) { + return new RoleView(tenantId, name, description, new HashSet<>(), new HashSet<>(), new HashSet<>()); + } + + public static RoleView from(final RoleId roleId, final String name, final String description) { + return new RoleView(roleId, name, description, new HashSet<>(), new HashSet<>(), new HashSet<>()); + } + + public static RoleView from(final RoleId roleId, final String name, final String description, final Set> permissions, final Set> groups, final Set> users) { + return new RoleView(roleId, name, description, permissions, groups, users); + } + + private RoleView(final TenantId tenantId, final String name, final String description, final Set> permissions, final Set> groups, final Set> users) { + this.id = null; + this.tenantId = tenantId.idString(); + this.name = name; + this.description = description; + this.permissions = permissions; + this.groups = groups; + this.users = users; + } + + private RoleView(final RoleId roleId, final String name, final String description, final Set> permissions, final Set> groups, final Set> users) { + this.id = roleId.idString(); + this.tenantId = roleId.tenantId.idString(); + this.name = name; + this.description = description; + this.permissions = permissions; + this.groups = groups; + this.users = users; + } + + public boolean isInRole(final UserId userId) { + return users.stream().filter(u -> u.left.equals(userId)).findFirst().isPresent(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RoleView)) return false; + RoleView roleView = (RoleView) o; + return id.equals(roleView.id) && tenantId.equals(roleView.tenantId) && name.equals(roleView.name) && description.equals(roleView.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, tenantId, name, description); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("tenantId", tenantId) + .append("name", name) + .append("description", description) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionActor.java new file mode 100644 index 00000000..8ef46e3c --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionActor.java @@ -0,0 +1,82 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.tenant.*; +import io.vlingo.xoom.auth.infrastructure.*; + +import io.vlingo.xoom.lattice.model.projection.Projectable; +import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.turbo.ComponentRegistry; + +/** + * See + * + * StateStoreProjectionActor + * + */ +public class TenantProjectionActor extends StateStoreProjectionActor { + + private static final TenantData Empty = TenantData.empty(); + + public TenantProjectionActor() { + this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store); + } + + public TenantProjectionActor(final StateStore stateStore) { + super(stateStore); + } + + @Override + protected TenantData currentDataFor(final Projectable projectable) { + return Empty; + } + + @Override + protected TenantData merge(final TenantData previousData, final int previousVersion, final TenantData currentData, final int currentVersion) { + + if (previousVersion == currentVersion) return currentData; + + TenantData merged = previousData; + + for (final Source event : sources()) { + switch (Events.valueOf(event.typeName())) { + case TenantSubscribed: { + final TenantSubscribed typedEvent = typed(event); + merged = TenantData.from(typedEvent.tenantId, typedEvent.name, typedEvent.description, typedEvent.active); + break; + } + + case TenantActivated: { + final TenantActivated typedEvent = typed(event); + merged = TenantData.from(typedEvent.tenantId, previousData.name, previousData.description, true); + break; + } + + case TenantDeactivated: { + final TenantDeactivated typedEvent = typed(event); + merged = TenantData.from(typedEvent.tenantId, previousData.name, previousData.description, false); + break; + } + + case TenantNameChanged: { + final TenantNameChanged typedEvent = typed(event); + merged = TenantData.from(typedEvent.tenantId, typedEvent.name, previousData.description, previousData.active); + break; + } + + case TenantDescriptionChanged: { + final TenantDescriptionChanged typedEvent = typed(event); + merged = TenantData.from(typedEvent.tenantId, previousData.name, typedEvent.description, previousData.active); + break; + } + + default: + logger().warn("Event of type " + event.typeName() + " was not matched."); + break; + } + } + + return merged; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueries.java new file mode 100644 index 00000000..9efd1088 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueries.java @@ -0,0 +1,14 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.Collection; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; + +import io.vlingo.xoom.auth.infrastructure.TenantData; + +@SuppressWarnings("all") +public interface TenantQueries { + Completes tenantOf(TenantId tenantId); + Completes> tenants(); +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesActor.java new file mode 100644 index 00000000..d22b9d29 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesActor.java @@ -0,0 +1,33 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.ArrayList; +import java.util.Collection; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.query.StateStoreQueryActor; +import io.vlingo.xoom.symbio.store.state.StateStore; + +import io.vlingo.xoom.auth.infrastructure.TenantData; + +/** + * See Querying a StateStore + */ +@SuppressWarnings("all") +public class TenantQueriesActor extends StateStoreQueryActor implements TenantQueries { + + public TenantQueriesActor(StateStore store) { + super(store); + } + + @Override + public Completes tenantOf(TenantId tenantId) { + return queryStateFor(tenantId.idString(), TenantData.class, TenantData.empty()); + } + + @Override + public Completes> tenants() { + return streamAllOf(TenantData.class, new ArrayList<>()); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionActor.java new file mode 100644 index 00000000..906afe2d --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionActor.java @@ -0,0 +1,163 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.Events; +import io.vlingo.xoom.auth.infrastructure.persistence.UserView.CredentialView; +import io.vlingo.xoom.auth.infrastructure.persistence.UserView.ProfileView; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.UserAssignedToRole; +import io.vlingo.xoom.auth.model.role.UserUnassignedFromRole; +import io.vlingo.xoom.auth.model.user.*; +import io.vlingo.xoom.lattice.model.projection.Projectable; +import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import java.util.Collections; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * See + * + * StateStoreProjectionActor + * + */ +public class UserProjectionActor extends StateStoreProjectionActor { + + private static final UserView Empty = UserView.empty(); + + public UserProjectionActor() { + this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store); + } + + public UserProjectionActor(final StateStore stateStore) { + super(stateStore); + } + + @Override + protected UserView currentDataFor(final Projectable projectable) { + return Empty; + } + + @Override + protected UserView merge(final UserView previousData, final int previousVersion, final UserView currentData, final int currentVersion) { + + if (previousVersion == currentVersion) return currentData; + + UserView merged = previousData; + + for (final Source event : sources()) { + switch (Events.valueOf(event.typeName())) { + case UserRegistered: { + final UserRegistered typedEvent = typed(event); + final ProfileView profile = ProfileView.from(typedEvent.profile); + final Set credentials = typedEvent.credentials.stream().map(c -> CredentialView.from(c)).collect(Collectors.toSet()); + merged = UserView.from(typedEvent.userId, typedEvent.username, profile, credentials, typedEvent.active, Collections.emptySet()); + break; + } + + case UserActivated: { + final UserActivated typedEvent = typed(event); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, true, previousData.roles); + break; + } + + case UserDeactivated: { + final UserDeactivated typedEvent = typed(event); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, false, previousData.roles); + break; + } + + case UserCredentialAdded: { + final UserCredentialAdded typedEvent = typed(event); + final CredentialView credential = CredentialView.from(typedEvent.credential); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, includeCredential(previousData.credentials, credential), previousData.active, previousData.roles); + break; + } + + case UserCredentialRemoved: { + final UserCredentialRemoved typedEvent = typed(event); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, removeCredential(previousData.credentials, typedEvent.authority), previousData.active, previousData.roles); + break; + } + + case UserCredentialReplaced: { + final UserCredentialReplaced typedEvent = typed(event); + final CredentialView credential = CredentialView.from(typedEvent.credential); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, includeCredential(removeCredential(previousData.credentials, typedEvent.authority), credential), previousData.active, previousData.roles); + break; + } + + case UserProfileReplaced: { + final UserProfileReplaced typedEvent = typed(event); + final ProfileView profile = ProfileView.from(typedEvent.profile); + merged = UserView.from(typedEvent.userId, previousData.username, profile, previousData.credentials, previousData.active, previousData.roles); + break; + } + + case UserAssignedToRole: { + final UserAssignedToRole typedEvent = typed(event); + final Set> roles = concat(previousData.roles, Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId)); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, previousData.active, roles); + break; + } + + case UserUnassignedFromRole: { + final UserUnassignedFromRole typedEvent = typed(event); + final Set> roles = filter(previousData.roles, r -> !r.equals(Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId))); + merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, previousData.active, roles); + break; + } + + default: + logger().warn("Event of type " + event.typeName() + " was not matched."); + break; + } + } + + return merged; + } + + @Override + protected String dataIdFor(final Projectable projectable) { + final Source firstEvent = sources().get(0); + switch (Events.valueOf(firstEvent.typeName())) { + case UserAssignedToRole: + return this.typed(firstEvent).userId.idString(); + case UserUnassignedFromRole: + return this.typed(firstEvent).userId.idString(); + default: + return super.dataIdFor(projectable); + } + } + + @Override + protected int currentDataVersionFor(final Projectable projectable, final UserView previousData, final int previousVersion) { + switch (Events.valueOf(sources().get(0).typeName())) { + case UserAssignedToRole: + case UserUnassignedFromRole: + return previousVersion + 1; + default: + return super.currentDataVersionFor(projectable, previousData, previousVersion); + } + } + + private Set includeCredential(final Set credentials, final CredentialView credential) { + return concat(credentials, credential); + } + + private Set removeCredential(final Set credentials, final String authority) { + return filter(credentials, c -> !c.authority.equals(authority)); + } + + private Set filter(final Set items, final Predicate predicate) { + return items.stream().filter(predicate).collect(Collectors.toSet()); + } + + private Set concat(final Set items, final T item) { + return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueries.java new file mode 100644 index 00000000..9b869b34 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueries.java @@ -0,0 +1,12 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import java.util.Collection; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; + +@SuppressWarnings("all") +public interface UserQueries { + Completes userOf(UserId userId); + Completes> users(); +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesActor.java new file mode 100644 index 00000000..35ae87c8 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesActor.java @@ -0,0 +1,31 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.query.StateStoreQueryActor; +import io.vlingo.xoom.symbio.store.state.StateStore; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * See Querying a StateStore + */ +@SuppressWarnings("all") +public class UserQueriesActor extends StateStoreQueryActor implements UserQueries { + + public UserQueriesActor(StateStore store) { + super(store); + } + + @Override + public Completes userOf(UserId userId) { + return queryStateFor(userId.idString(), UserView.class, UserView.empty()); + } + + @Override + public Completes> users() { + return streamAllOf(UserView.class, new ArrayList<>()); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserView.java new file mode 100644 index 00000000..d85396c4 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserView.java @@ -0,0 +1,247 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.PersonName; +import io.vlingo.xoom.auth.model.value.Profile; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class UserView { + public final String id; + public final String tenantId; + public final String username; + public final ProfileView profile; + public final boolean active; + public final Set credentials; + public final Set> roles; + + public static UserView empty() { + return from(UserId.from(TenantId.from(""), ""), "", null, Collections.emptySet(), false, Collections.emptySet()); + } + + public static UserView from(final UserId userId, final String username, final ProfileView profile, final Set credentials, final boolean active, final Set> roles) { + return new UserView(userId, username, profile, credentials, active, roles); + } + + public static UserView from(final UserId userId, final String username, final ProfileView profile, final CredentialView credential, final boolean active, final Set> roles) { + return new UserView(userId, username, profile, Stream.of(credential).collect(Collectors.toSet()), active, roles); + } + + private UserView(String id, String tenantId, String username, ProfileView profile, Set credentials, boolean active, final Set> roles) { + this.id = id; + this.tenantId = tenantId; + this.username = username; + this.profile = profile; + this.active = active; + this.credentials = credentials; + this.roles = roles; + } + + private UserView(UserId userId, String username, ProfileView profile, Set credentials, boolean active, final Set> roles) { + this(userId.idString(), userId.tenantId.id, username, profile, credentials, active, roles); + } + + public boolean isInRole(final RoleId roleId) { + return roles.stream().filter(r -> r.right.equals(roleId)).findFirst().isPresent(); + } + + public static class CredentialView { + public final String authority; + public final String id; + public final String secret; + public final String type; + + public static CredentialView empty() { + return new CredentialView(null, null, null, null); + } + + public static CredentialView from(final Credential credential) { + if (credential == null) { + return CredentialView.empty(); + } else { + return from(credential.authority, credential.id, credential.secret, credential.type.name()); + } + } + + public static CredentialView from(final String authority, final String id, final String secret) { + return from(authority, id, secret, null); + } + + public static CredentialView from(final String authority, final String id, final String secret, final String type) { + return new CredentialView(authority, id, secret, type); + } + + public static Set fromAll(final Set correspondingObjects) { + return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(CredentialView::from).collect(Collectors.toSet()); + } + + private CredentialView(String authority, String id, String secret, String type) { + this.authority = authority; + this.id = id; + this.secret = secret; + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CredentialView)) return false; + CredentialView that = (CredentialView) o; + return Objects.equals(authority, that.authority) && Objects.equals(id, that.id) && Objects.equals(secret, that.secret) && Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(authority, id, secret, type); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("authority", authority) + .append("secret", secret) + .append("type", type) + .toString(); + } + } + + public static class ProfileView { + public final String emailAddress; + public final PersonNameView name; + public final String phone; + + public static ProfileView empty() { + return new ProfileView(null, null, null); + } + + public static ProfileView from(final Profile profile) { + if (profile == null) { + return ProfileView.empty(); + } else { + final PersonNameView name = profile.name != null ? PersonNameView.from(profile.name) : null; + return from(name, profile.emailAddress, profile.phone); + } + } + + public static ProfileView from(final PersonNameView name, final String emailAddress, final String phone) { + return new ProfileView(emailAddress, name, phone); + } + + public static ProfileView from(final String emailAddress, final PersonNameView name, final String phone) { + return new ProfileView(emailAddress, name, phone); + } + + private ProfileView(final String emailAddress, final PersonNameView name, final String phone) { + this.emailAddress = emailAddress; + this.name = name; + this.phone = phone; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileView)) return false; + ProfileView that = (ProfileView) o; + return Objects.equals(emailAddress, that.emailAddress) && Objects.equals(name, that.name) && Objects.equals(phone, that.phone); + } + + @Override + public int hashCode() { + return Objects.hash(emailAddress, name, phone); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("emailAddress", emailAddress) + .append("name", name) + .append("phone", phone) + .toString(); + } + } + + public static class PersonNameView { + public final String given; + public final String family; + public final String second; + + public static PersonNameView empty() { + return new PersonNameView(null, null, null); + } + + public static PersonNameView from(final PersonName personName) { + if (personName == null) { + return PersonNameView.empty(); + } else { + return from(personName.given, personName.family, personName.second); + } + } + + public static PersonNameView from(final String given, final String family, final String second) { + return new PersonNameView(given, family, second); + } + + private PersonNameView(final String given, final String family, final String second) { + this.given = given; + this.family = family; + this.second = second; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PersonNameView)) return false; + PersonNameView that = (PersonNameView) o; + return Objects.equals(given, that.given) && Objects.equals(family, that.family) && Objects.equals(second, that.second); + } + + @Override + public int hashCode() { + return Objects.hash(given, family, second); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("given", given) + .append("family", family) + .append("second", second) + .toString(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserView)) return false; + UserView userView = (UserView) o; + return active == userView.active && Objects.equals(id, userView.id) && Objects.equals(tenantId, userView.tenantId) && Objects.equals(username, userView.username) && Objects.equals(profile, userView.profile) && Objects.equals(credentials, userView.credentials); + } + + @Override + public int hashCode() { + return Objects.hash(id, tenantId, username, profile, active, credentials); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .append("tenantId", tenantId) + .append("username", username) + .append("profile", profile) + .append("active", active) + .append("credentials", credentials) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddress.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddress.java new file mode 100644 index 00000000..706fba6a --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddress.java @@ -0,0 +1,60 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Address; + +import java.util.function.Function; + +public class ComplexAddress implements Address { + private final ID id; + private final Function transformer; + + public ComplexAddress(ID id, Function transformer) { + this.id = id; + this.transformer = transformer; + } + + @Override + public long id() { + byte[] idBytes = transformer.apply(id).getBytes(); + long outcome = 0; + for (int i = 0; i < idBytes.length; i++) { + outcome = ((outcome << 8) | (idBytes[i] & 0xFF)); + } + return outcome; + } + + @Override + public long idSequence() { + return id(); + } + + @Override + public String idSequenceString() { + return idString(); + } + + @Override + public String idString() { + return String.valueOf(id()); + } + + @Override + public T idTyped() { + return (T) id; + } + + @Override + public String name() { + return "complex"; + } + + @Override + public boolean isDistributable() { + return true; + } + + @Override + public int compareTo(Address o) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResource.java new file mode 100644 index 00000000..010dbf82 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResource.java @@ -0,0 +1,176 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Address; +import io.vlingo.xoom.actors.Definition; +import io.vlingo.xoom.auth.infrastructure.GroupData; +import io.vlingo.xoom.auth.infrastructure.persistence.GroupQueries; +import io.vlingo.xoom.auth.infrastructure.persistence.GroupView; +import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider; +import io.vlingo.xoom.auth.model.group.Group; +import io.vlingo.xoom.auth.model.group.GroupEntity; +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.http.ContentType; +import io.vlingo.xoom.http.Response; +import io.vlingo.xoom.http.ResponseHeader; +import io.vlingo.xoom.http.resource.DynamicResourceHandler; +import io.vlingo.xoom.http.resource.Resource; +import io.vlingo.xoom.lattice.grid.Grid; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import java.nio.CharBuffer; +import java.util.UUID; + +import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; +import static io.vlingo.xoom.http.Response.Status.*; +import static io.vlingo.xoom.http.ResponseHeader.Location; +import static io.vlingo.xoom.http.resource.ResourceBuilder.resource; + +/** + * See @ResourceHandlers + */ +public class GroupResource extends DynamicResourceHandler { + private final Grid grid; + private final GroupQueries $queries; + + public GroupResource(final Grid grid) { + super(grid.world().stage()); + this.grid = grid; + this.$queries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).groupQueries; + } + + public Completes provisionGroup(final GroupData data) { + return create(data.tenantId, data.name) + .provisionGroup(data.name, data.description) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.id))), serialized(GroupView.from(state)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()))); + } + + public Completes changeDescription(final String tenantId, final String groupName, final GroupData data) { + return resolve(tenantId, groupName) + .andThenTo(group -> group.changeDescription(data.description)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes assignGroup(final String tenantId, final String groupName, final String innerGroupName, final GroupData data) { + return resolve(tenantId, groupName) + .andThenTo(group -> group.assignGroup(GroupId.from(TenantId.from(tenantId), innerGroupName))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes unassignGroup(final String tenantId, final String groupName, final String innerGroupName, final GroupData data) { + return resolve(tenantId, groupName) + .andThenTo(group -> group.unassignGroup(GroupId.from(TenantId.from(tenantId), innerGroupName))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes assignUser(final String tenantId, final String groupName, final String username, final GroupData data) { + return resolve(tenantId, groupName) + .andThenTo(group -> group.assignUser(UserId.from(TenantId.from(data.tenantId), username))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes unassignUser(final String tenantId, final String groupName, final String username, final GroupData data) { + return resolve(tenantId, groupName) + .andThenTo(group -> group.unassignUser(UserId.from(TenantId.from(data.tenantId), username))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes groups() { + return $queries.groups() + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes groupOf(final String tenantId, final String groupName) { + return $queries.groupOf(GroupId.from(TenantId.from(tenantId), groupName)) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + @Override + public Resource routes() { + return resource("GroupResource", + io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/groups") + .body(GroupData.class) + .handle(this::provisionGroup), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/groups/{groupName}/description") + .param(String.class) + .param(String.class) + .body(GroupData.class) + .handle(this::changeDescription), + io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/groups/{groupName}/groups/{innerGroupName}") + .param(String.class) + .param(String.class) + .param(String.class) + .body(GroupData.class) + .handle(this::assignGroup), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/groups/{groupName}/groups/{innerGroupName}") + .param(String.class) + .param(String.class) + .param(String.class) + .body(GroupData.class) + .handle(this::unassignGroup), + io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/groups/{groupName}/users/{username}") + .param(String.class) + .param(String.class) + .param(String.class) + .body(GroupData.class) + .handle(this::assignUser), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/groups/{groupName}/users/{username}") + .param(String.class) + .param(String.class) + .param(String.class) + .body(GroupData.class) + .handle(this::unassignUser), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/groups") + .handle(this::groups), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/groups/{groupName}") + .param(String.class) + .param(String.class) + .handle(this::groupOf) + ); + } + + @Override + protected ContentType contentType() { + return ContentType.of("application/json", "charset=UTF-8"); + } + + private String location(final GroupId groupId) { + return String.format("/tenants/%s/groups/%s", groupId.tenantId.id, groupId.groupName); + } + + private Completes resolve(final String tenantId, final String groupName) { + final GroupId groupId = GroupId.from(TenantId.from(tenantId), groupName); + final Address address = new GroupAddress(groupId); + return grid.actorOf(Group.class, address, Definition.has(GroupEntity.class, Definition.parameters(groupId))); + } + + private Group create(final String tenantId, final String groupName) { + final GroupId groupId = GroupId.from(TenantId.from(tenantId), groupName); + final Address address = new GroupAddress(groupId); + return grid.actorFor(Group.class, Definition.has(GroupEntity.class, Definition.parameters(groupId)), address); + } + + private class GroupAddress extends ComplexAddress { + public GroupAddress(final GroupId groupId) { + super(groupId, (id) -> id.idString()); + } + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResource.java new file mode 100644 index 00000000..00ebf741 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResource.java @@ -0,0 +1,157 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Address; +import io.vlingo.xoom.actors.Definition; +import io.vlingo.xoom.auth.infrastructure.ConstraintData; +import io.vlingo.xoom.auth.infrastructure.PermissionData; +import io.vlingo.xoom.auth.infrastructure.persistence.PermissionQueries; +import io.vlingo.xoom.auth.infrastructure.persistence.PermissionView; +import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider; +import io.vlingo.xoom.auth.model.permission.Permission; +import io.vlingo.xoom.auth.model.permission.PermissionEntity; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.http.ContentType; +import io.vlingo.xoom.http.Response; +import io.vlingo.xoom.http.ResponseHeader; +import io.vlingo.xoom.http.resource.DynamicResourceHandler; +import io.vlingo.xoom.http.resource.Resource; +import io.vlingo.xoom.lattice.grid.Grid; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; +import static io.vlingo.xoom.http.Response.Status.*; +import static io.vlingo.xoom.http.ResponseHeader.Location; +import static io.vlingo.xoom.http.resource.ResourceBuilder.resource; + +/** + * See @ResourceHandlers + */ +public class PermissionResource extends DynamicResourceHandler { + private final Grid grid; + private final PermissionQueries $queries; + + public PermissionResource(final Grid grid) { + super(grid.world().stage()); + this.grid = grid; + this.$queries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).permissionQueries; + } + + public Completes provisionPermission(final PermissionData data) { + return create(data.tenantId, data.name) + .provisionPermission(data.name, data.description) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.id))), serialized(PermissionView.from(state)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()))); + } + + public Completes enforce(final String tenantId, final String permissionName, final ConstraintData data) { + return resolve(tenantId, permissionName) + .andThenTo(permission -> permission.enforce(data.toConstraint())) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes enforceReplacement(final String tenantId, final String permissionName, final String constraintName, final ConstraintData data) { + return resolve(tenantId, permissionName) + .andThenTo(permission -> permission.enforceReplacement(constraintName, data.toConstraint())) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes forget(final String tenantId, final String permissionName, final String constraintName) { + return resolve(tenantId, permissionName) + .andThenTo(permission -> permission.forget(constraintName)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes changeDescription(final String tenantId, final String permissionName, final PermissionData data) { + return resolve(tenantId, permissionName) + .andThenTo(permission -> permission.changeDescription(data.description)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes permissions() { + return $queries.permissions() + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes permissionOf(final String tenantId, final String permissionName) { + return $queries.permissionOf(PermissionId.from(TenantId.from(tenantId), permissionName)) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + @Override + public Resource routes() { + return resource("PermissionResource", + io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/permissions") + .body(PermissionData.class) + .handle(this::provisionPermission), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/permissions/{permissionName}/constraints") + .param(String.class) + .param(String.class) + .body(ConstraintData.class) + .handle(this::enforce), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/permissions/{permissionName}/constraints/{constraintName}") + .param(String.class) + .param(String.class) + .param(String.class) + .body(ConstraintData.class) + .handle(this::enforceReplacement), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/permissions/{permissionName}/constraints/{constraintName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::forget), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/permissions/{permissionName}/description") + .param(String.class) + .param(String.class) + .body(PermissionData.class) + .handle(this::changeDescription), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/permissions") + .handle(this::permissions), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/permissions/{permissionName}") + .param(String.class) + .param(String.class) + .handle(this::permissionOf) + ); + } + + @Override + protected ContentType contentType() { + return ContentType.of("application/json", "charset=UTF-8"); + } + + private String location(final PermissionId permissionId) { + return String.format("/tenants/%s/permissions/%s", permissionId.tenantId.id, permissionId.permissionName); + } + + private Completes resolve(final String tenantId, final String permissionName) { + final PermissionId permissionId = PermissionId.from(TenantId.from(tenantId), permissionName); + final Address address = new PermissionAddress(permissionId); + return grid.actorOf(Permission.class, address, Definition.has(PermissionEntity.class, Definition.parameters(permissionId))); + } + + private Permission create(final String tenantId, final String permissionName) { + final PermissionId permissionId = PermissionId.from(TenantId.from(tenantId), permissionName); + final Address address = new PermissionAddress(permissionId); + return grid.actorFor(Permission.class, Definition.has(PermissionEntity.class, Definition.parameters(permissionId)), address); + } + + private class PermissionAddress extends ComplexAddress { + public PermissionAddress(final PermissionId permissionId) { + super(permissionId, (id) -> id.idString()); + } + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResource.java new file mode 100644 index 00000000..d707a2df --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResource.java @@ -0,0 +1,262 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Address; +import io.vlingo.xoom.actors.Definition; +import io.vlingo.xoom.auth.infrastructure.*; +import io.vlingo.xoom.auth.infrastructure.persistence.*; +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.role.Role; +import io.vlingo.xoom.auth.model.role.RoleEntity; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.http.ContentType; +import io.vlingo.xoom.http.Response; +import io.vlingo.xoom.http.ResponseHeader; +import io.vlingo.xoom.http.resource.DynamicResourceHandler; +import io.vlingo.xoom.http.resource.Resource; +import io.vlingo.xoom.lattice.grid.Grid; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; +import static io.vlingo.xoom.http.Response.Status.*; +import static io.vlingo.xoom.http.ResponseHeader.Location; +import static io.vlingo.xoom.http.resource.ResourceBuilder.resource; + +/** + * See @ResourceHandlers + */ +public class RoleResource extends DynamicResourceHandler { + private final Grid grid; + private final RoleQueries $roleQueries; + private final PermissionQueries $permissionQueries; + private final GroupQueries $groupQueries; + private final UserQueries $userQueries; + + public RoleResource(final Grid grid) { + super(grid.world().stage()); + this.grid = grid; + this.$roleQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).roleQueries; + this.$permissionQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).permissionQueries; + this.$groupQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).groupQueries; + this.$userQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).userQueries; + } + + public Completes provisionRole(final RoleData data) { + return create(data.tenantId, data.name) + .provisionRole(data.name, data.description) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.roleId))), serialized(RoleView.from(state)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()))); + } + + public Completes changeDescription(final String tenantId, final String roleName, final RoleData data) { + return resolve(tenantId, roleName) + .andThenTo(role -> role.changeDescription(data.description)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes assignGroup(final String tenantId, final String roleName, final GroupData groupData) { + final GroupId groupId = GroupId.from(TenantId.from(tenantId), groupData.name); + return resolve(tenantId, roleName) + .andThenTo(role -> role.assignGroup(groupId)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, roleGroupLocation(state.roleId, groupId)))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes unassignGroup(final String tenantId, final String roleName, final String groupName) { + return resolve(tenantId, roleName) + .andThenTo(role -> role.unassignGroup(GroupId.from(TenantId.from(tenantId), groupName))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes assignUser(final String tenantId, final String roleName, final UserRegistrationData userData) { + final UserId userId = UserId.from(TenantId.from(tenantId), userData.username); + return resolve(tenantId, roleName) + .andThenTo(role -> role.assignUser(userId)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, roleUserLocation(state.roleId, userId)))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes unassignUser(final String tenantId, final String roleName, final String username) { + return resolve(tenantId, roleName) + .andThenTo(role -> role.unassignUser(UserId.from(TenantId.from(tenantId), username))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes attach(final String tenantId, final String roleName, final PermissionData permissionData) { + final PermissionId permissionId = PermissionId.from(TenantId.from(tenantId), permissionData.name); + return resolve(tenantId, roleName) + .andThenTo(role -> role.attach(permissionId)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, rolePermissionLocation(state.roleId, permissionId)))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes detach(final String tenantId, final String roleName, final String permissionName) { + return resolve(tenantId, roleName) + .andThenTo(role -> role.detach(PermissionId.from(TenantId.from(tenantId), permissionName))) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes roles() { + return $roleQueries.roles() + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes roleOf(final String tenantId, final String roleName) { + return $roleQueries.roleOf(RoleId.from(TenantId.from(tenantId), roleName)) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes permissionOf(final String tenantIdString, final String roleName, final String permissionName) { + final TenantId tenantId = TenantId.from(tenantIdString); + final RoleId roleId = RoleId.from(tenantId, roleName); + final PermissionId permissionId = PermissionId.from(tenantId, permissionName); + return $permissionQueries.permissionOf(permissionId) + .andThen(permissionView -> permissionView.isAttachedTo(roleId) ? permissionView : null) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes groupOf(final String tenantId, final String roleName, final String groupName) { + final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName); + final GroupId groupId = GroupId.from(roleId.tenantId, groupName); + return $groupQueries.groupOf(groupId) + .andThen(groupView -> groupView.isInRole(roleId) ? groupView : null) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes userOf(final String tenantId, final String roleName, final String username) { + final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName); + final UserId userId = UserId.from(roleId.tenantId, username); + return $userQueries.userOf(userId) + .andThen(userView -> userView.isInRole(roleId) ? userView : null) + .andThenTo(userView -> Completes.withSuccess(entityResponseOf(Ok, serialized(MinimalUserData.from(userView))))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + @Override + public Resource routes() { + return resource("RoleResource", + io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/roles") + .body(RoleData.class) + .handle(this::provisionRole), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/roles/{roleName}/description") + .param(String.class) + .param(String.class) + .body(RoleData.class) + .handle(this::changeDescription), + io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/roles/{roleName}/groups") + .param(String.class) + .param(String.class) + .body(GroupData.class) + .handle(this::assignGroup), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/roles/{roleName}/groups/{groupName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::unassignGroup), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}/groups/{groupName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::groupOf), + io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/roles/{roleName}/users") + .param(String.class) + .param(String.class) + .body(UserRegistrationData.class) + .handle(this::assignUser), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/roles/{roleName}/users/{username}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::unassignUser), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}/users/{username}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::userOf), + io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/roles/{roleName}/permissions") + .param(String.class) + .param(String.class) + .body(PermissionData.class) + .handle(this::attach), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/roles/{roleName}/permissions/{permissionName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::detach), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}/permissions/{permissionName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::permissionOf), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles") + .handle(this::roles), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}") + .param(String.class) + .param(String.class) + .handle(this::roleOf) + ); + } + + @Override + protected ContentType contentType() { + return ContentType.of("application/json", "charset=UTF-8"); + } + + private String location(final RoleId roleId) { + return String.format("/tenants/%s/roles/%s", roleId.tenantId.id, roleId.roleName); + } + + private String roleGroupLocation(final RoleId roleId, final GroupId groupId) { + return String.format("/tenants/%s/roles/%s/groups/%s", roleId.tenantId.id, roleId.roleName, groupId.groupName); + } + + private String roleUserLocation(final RoleId roleId, final UserId userId) { + return String.format("/tenants/%s/roles/%s/users/%s", roleId.tenantId.id, roleId.roleName, userId.username); + } + + private String rolePermissionLocation(final RoleId roleId, final PermissionId permissionId) { + return String.format("/tenants/%s/roles/%s/permissions/%s", roleId.tenantId.id, roleId.roleName, permissionId.permissionName); + } + + private Completes resolve(final String tenantId, String roleName) { + final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName); + final Address address = new RoleAddress(roleId); + return grid.actorOf(Role.class, address, Definition.has(RoleEntity.class, Definition.parameters(roleId))); + } + + private Role create(String tenantId, String roleName) { + final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName); + final Address address = new RoleAddress(roleId); + return grid.actorFor(Role.class, Definition.has(RoleEntity.class, Definition.parameters(roleId)), address); + } + + private class RoleAddress extends ComplexAddress { + public RoleAddress(final RoleId roleId) { + super(roleId, (id) -> id.idString()); + } + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResource.java new file mode 100644 index 00000000..bb771b3b --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResource.java @@ -0,0 +1,140 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Definition; +import io.vlingo.xoom.actors.Address; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.turbo.ComponentRegistry; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.http.ContentType; +import io.vlingo.xoom.http.Response; +import io.vlingo.xoom.http.ResponseHeader; +import io.vlingo.xoom.http.resource.Resource; +import io.vlingo.xoom.http.resource.DynamicResourceHandler; +import io.vlingo.xoom.lattice.grid.Grid; +import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider; +import io.vlingo.xoom.auth.infrastructure.*; +import io.vlingo.xoom.auth.model.tenant.TenantEntity; +import io.vlingo.xoom.auth.infrastructure.persistence.TenantQueries; +import io.vlingo.xoom.auth.model.tenant.Tenant; + +import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; +import static io.vlingo.xoom.http.Response.Status.*; +import static io.vlingo.xoom.http.ResponseHeader.Location; +import static io.vlingo.xoom.http.resource.ResourceBuilder.resource; + +/** + * See @ResourceHandlers + */ +public class TenantResource extends DynamicResourceHandler { + private final Grid grid; + private final TenantQueries $queries; + + public TenantResource(final Grid grid) { + super(grid.world().stage()); + this.grid = grid; + this.$queries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).tenantQueries; + } + + public Completes subscribeFor(final TenantData data) { + return create() + .subscribeFor(data.name, data.description, data.active) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.tenantId))), serialized(TenantData.from(state)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()))); + } + + public Completes activate(final String id) { + return resolve(id) + .andThenTo(tenant -> tenant.activate()) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes deactivate(final String id) { + return resolve(id) + .andThenTo(tenant -> tenant.deactivate()) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes changeDescription(final String id, final TenantData data) { + return resolve(id) + .andThenTo(tenant -> tenant.changeDescription(data.description)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes changeName(final String id, final TenantData data) { + return resolve(id) + .andThenTo(tenant -> tenant.changeName(data.name)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes tenants() { + return $queries.tenants() + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes tenantOf(final String id) { + return $queries.tenantOf(TenantId.from(id)) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + @Override + public Resource routes() { + return resource("TenantResource", + io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants") + .body(TenantData.class) + .handle(this::subscribeFor), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/activate") + .param(String.class) + .handle(this::activate), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/deactivate") + .param(String.class) + .handle(this::deactivate), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/description") + .param(String.class) + .body(TenantData.class) + .handle(this::changeDescription), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/name") + .param(String.class) + .body(TenantData.class) + .handle(this::changeName), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants") + .handle(this::tenants), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{id}") + .param(String.class) + .handle(this::tenantOf) + ); + } + + @Override + protected ContentType contentType() { + return ContentType.of("application/json", "charset=UTF-8"); + } + + private String location(final TenantId tenantId) { + return String.format("/tenants/%s", tenantId.id); + } + + private Completes resolve(final String id) { + final TenantId tenantId = TenantId.from(id); + final Address address = grid.addressFactory().from(tenantId.idString()); + return grid.actorOf(Tenant.class, address, Definition.has(TenantEntity.class, Definition.parameters(tenantId))); + } + + private Tenant create() { + final Address address = grid.addressFactory().uniquePrefixedWith("g-"); + final TenantId tenantId = TenantId.from(address.idString()); + return grid.actorFor(Tenant.class, Definition.has(TenantEntity.class, Definition.parameters(tenantId)), address); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/UserResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/UserResource.java new file mode 100644 index 00000000..ec178ca0 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/UserResource.java @@ -0,0 +1,230 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Address; +import io.vlingo.xoom.actors.Definition; +import io.vlingo.xoom.auth.infrastructure.CredentialData; +import io.vlingo.xoom.auth.infrastructure.ProfileData; +import io.vlingo.xoom.auth.infrastructure.UserRegistrationData; +import io.vlingo.xoom.auth.infrastructure.persistence.PermissionQueries; +import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider; +import io.vlingo.xoom.auth.infrastructure.persistence.RoleQueries; +import io.vlingo.xoom.auth.infrastructure.persistence.UserQueries; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.User; +import io.vlingo.xoom.auth.model.user.UserEntity; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.PersonName; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.http.ContentType; +import io.vlingo.xoom.http.Response; +import io.vlingo.xoom.http.ResponseHeader; +import io.vlingo.xoom.http.resource.DynamicResourceHandler; +import io.vlingo.xoom.http.resource.Resource; +import io.vlingo.xoom.lattice.grid.Grid; +import io.vlingo.xoom.turbo.ComponentRegistry; + +import java.util.Set; +import java.util.stream.Collectors; + +import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; +import static io.vlingo.xoom.http.Response.Status.*; +import static io.vlingo.xoom.http.ResponseHeader.Location; +import static io.vlingo.xoom.http.resource.ResourceBuilder.resource; + +/** + * See @ResourceHandlers + */ +public class UserResource extends DynamicResourceHandler { + private final Grid grid; + private final UserQueries $userQueries; + private final RoleQueries $roleQueries; + private final PermissionQueries $permissionQueries; + + public UserResource(final Grid grid) { + super(grid.world().stage()); + this.grid = grid; + this.$userQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).userQueries; + this.$roleQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).roleQueries; + this.$permissionQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).permissionQueries; + } + + public Completes registerUser(final UserRegistrationData data) { + final PersonName name = PersonName.from(data.profile.name.given, data.profile.name.family, data.profile.name.second); + final Profile profile = Profile.from(data.profile.emailAddress, name, data.profile.phone); + final Set credentials = data.credentials.stream().map(CredentialData::toCredential).collect(Collectors.toSet()); + return create(data.tenantId, data.username) + .registerUser(data.username, profile, credentials, data.active) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.userId))), serialized(UserRegistrationData.from(state)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()))); + } + + public Completes activate(final String tenantId, final String username, final UserRegistrationData data) { + return resolve(tenantId, username) + .andThenTo(user -> user.activate()) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes deactivate(final String tenantId, final String username, final UserRegistrationData data) { + return resolve(tenantId, username) + .andThenTo(user -> user.deactivate()) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes addCredential(final String tenantId, final String username, final CredentialData data) { + return resolve(tenantId, username) + .andThenTo(user -> user.addCredential(data.toCredential())) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes removeCredential(final String tenantId, final String username, final String authority) { + return resolve(tenantId, username) + .andThenTo(user -> user.removeCredential(authority)) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes replaceCredential(final String tenantId, final String username, final String authority, final CredentialData data) { + return resolve(tenantId, username) + .andThenTo(user -> user.replaceCredential(authority, data.toCredential())) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes replaceProfile(final String tenantId, final String username, final ProfileData data) { + return resolve(tenantId, username) + .andThenTo(user -> user.replaceProfile(data.toProfile())) + .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state))))) + .otherwise(noGreeting -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes users() { + return $userQueries.users() + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes userOf(final String tenantId, final String username) { + return $userQueries.userOf(UserId.from(TenantId.from(tenantId), username)) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes roleOf(final String tenantId, final String username, final String roleName) { + final UserId userId = UserId.from(TenantId.from(tenantId), username); + final RoleId roleId = RoleId.from(userId.tenantId, roleName); + return $roleQueries.roleOf(roleId) + .andThen(roleView -> roleView.isInRole(userId) ? roleView : null) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + public Completes permissionOf(final String tenantId, final String username, final String permissionName) { + final UserId userId = UserId.from(TenantId.from(tenantId), username); + final PermissionId permissionId = PermissionId.from(userId.tenantId, permissionName); + return $permissionQueries.permissionOf(permissionId) + .andThen(permissionView -> permissionView.isAttachedTo(userId) ? permissionView : null) + .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data)))) + .otherwise(arg -> Response.of(NotFound)) + .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())); + } + + @Override + public Resource routes() { + return resource("UserResource", + io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/users") + .body(UserRegistrationData.class) + .handle(this::registerUser), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/activate") + .param(String.class) + .param(String.class) + .body(UserRegistrationData.class) + .handle(this::activate), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/deactivate") + .param(String.class) + .param(String.class) + .body(UserRegistrationData.class) + .handle(this::deactivate), + io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/users/{username}/credentials") + .param(String.class) + .param(String.class) + .body(CredentialData.class) + .handle(this::addCredential), + io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/users/{username}/credentials/{authority}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::removeCredential), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/credentials/{authority}") + .param(String.class) + .param(String.class) + .param(String.class) + .body(CredentialData.class) + .handle(this::replaceCredential), + io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/profile") + .param(String.class) + .param(String.class) + .body(ProfileData.class) + .handle(this::replaceProfile), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users/{username}/roles/{roleName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::roleOf), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users/{username}/permissions/{permissionName}") + .param(String.class) + .param(String.class) + .param(String.class) + .handle(this::permissionOf), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users") + .handle(this::users), + io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users/{username}") + .param(String.class) + .param(String.class) + .handle(this::userOf) + ); + } + + @Override + protected ContentType contentType() { + return ContentType.of("application/json", "charset=UTF-8"); + } + + private String location(final UserId userId) { + return String.format("/tenants/%s/users/%s", userId.tenantId.id, userId.username); + } + + private Completes resolve(final String tenantId, final String username) { + final UserId userId = UserId.from(TenantId.from(tenantId), username); + final Address address = new UserAddress(userId); + return grid.actorOf(User.class, address, Definition.has(UserEntity.class, Definition.parameters(userId))); + } + + private User create(final String tenantId, final String username) { + final UserId userId = UserId.from(TenantId.from(tenantId), username); + final Address address = new UserAddress(userId); + return grid.actorFor(User.class, Definition.has(UserEntity.class, Definition.parameters(userId)), address); + } + + private class UserAddress extends ComplexAddress { + public UserAddress(final UserId userId) { + super(userId, (id) -> id.idString()); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/Group.java b/src/main/java/io/vlingo/xoom/auth/model/group/Group.java new file mode 100644 index 00000000..08d5711b --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/Group.java @@ -0,0 +1,19 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; + +public interface Group { + + Completes provisionGroup(final String name, final String description); + + Completes changeDescription(final String description); + + Completes assignGroup(final GroupId groupId); + + Completes unassignGroup(final GroupId groupId); + + Completes assignUser(final UserId userId); + + Completes unassignUser(final UserId userId); +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupAssignedToGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupAssignedToGroup.java new file mode 100644 index 00000000..7e10231e --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupAssignedToGroup.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class GroupAssignedToGroup extends IdentifiedDomainEvent { + + public final GroupId groupId; + public final GroupId innerGroupId; + + public GroupAssignedToGroup(final GroupId groupId, final GroupId innerGroupId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.groupId = groupId; + this.innerGroupId = innerGroupId; + } + + @Override + public String identity() { + return groupId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupDescriptionChanged.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupDescriptionChanged.java new file mode 100644 index 00000000..3765fcc1 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupDescriptionChanged.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class GroupDescriptionChanged extends IdentifiedDomainEvent { + + public final GroupId groupId; + public final String description; + + public GroupDescriptionChanged(final GroupId groupId, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.groupId = groupId; + this.description = description; + } + + @Override + public String identity() { + return groupId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupEntity.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupEntity.java new file mode 100644 index 00000000..43e2844d --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupEntity.java @@ -0,0 +1,107 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.EventSourced; + +/** + * See EventSourced + */ +public final class GroupEntity extends EventSourced implements Group { + private GroupState state; + + public GroupEntity(final GroupId groupId) { + super(groupId.idString()); + this.state = GroupState.identifiedBy(groupId); + } + + static { + EventSourced.registerConsumer(GroupEntity.class, GroupProvisioned.class, GroupEntity::applyGroupProvisioned); + EventSourced.registerConsumer(GroupEntity.class, GroupDescriptionChanged.class, GroupEntity::applyGroupDescriptionChanged); + EventSourced.registerConsumer(GroupEntity.class, GroupAssignedToGroup.class, GroupEntity::applyGroupAssignedToGroup); + EventSourced.registerConsumer(GroupEntity.class, GroupUnassignedFromGroup.class, GroupEntity::applyGroupUnassignedFromGroup); + EventSourced.registerConsumer(GroupEntity.class, UserAssignedToGroup.class, GroupEntity::applyUserAssignedToGroup); + EventSourced.registerConsumer(GroupEntity.class, UserUnassignedFromGroup.class, GroupEntity::applyUserUnassignedFromGroup); + } + + @Override + public Completes provisionGroup(final String name, final String description) { + return apply(new GroupProvisioned(state.id, name, description), () -> state); + } + + @Override + public Completes changeDescription(final String description) { + return apply(new GroupDescriptionChanged(state.id, description), () -> state); + } + + @Override + public Completes assignGroup(GroupId groupId) { + return apply(new GroupAssignedToGroup(state.id, groupId), () -> state); + } + + @Override + public Completes unassignGroup(final GroupId groupId) { + return apply(new GroupUnassignedFromGroup(state.id, groupId), () -> state); + } + + @Override + public Completes assignUser(final UserId userId) { + return apply(new UserAssignedToGroup(state.id, userId), () -> state); + } + + @Override + public Completes unassignUser(final UserId userId) { + return apply(new UserUnassignedFromGroup(state.id, userId), () -> state); + } + + private void applyGroupProvisioned(final GroupProvisioned event) { + state = state.provisionGroup(event.name, event.description); + } + + private void applyGroupDescriptionChanged(final GroupDescriptionChanged event) { + state = state.changeDescription(event.description); + } + + private void applyGroupAssignedToGroup(final GroupAssignedToGroup event) { + state = state.assignGroup(event.innerGroupId); + } + + private void applyGroupUnassignedFromGroup(final GroupUnassignedFromGroup event) { + state = state.unassignGroup(event.innerGroupId); + } + + private void applyUserAssignedToGroup(final UserAssignedToGroup event) { + state = state.assignUser(event.userId); + } + + private void applyUserUnassignedFromGroup(final UserUnassignedFromGroup event) { + state = state.unassignUser(event.userId); + } + + /* + * Restores my initial state by means of {@code state}. + * + * @param snapshot the {@code GroupState} holding my state + * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed + */ + @Override + @SuppressWarnings("hiding") + protected void restoreSnapshot(final GroupState snapshot, final int currentVersion) { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + } + + /* + * Answer the valid {@code GroupState} instance if a snapshot should + * be taken and persisted along with applied {@code Source} instance(s). + * + * @return GroupState + */ + @Override + @SuppressWarnings("unchecked") + protected GroupState snapshot() { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + return null; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupId.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupId.java new file mode 100644 index 00000000..efa8f3a4 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupId.java @@ -0,0 +1,55 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Objects; + +public class GroupId { + public final TenantId tenantId; + public final String groupName; + + public static GroupId from(final TenantId tenantId, final String groupName) { + return new GroupId(tenantId, groupName); + } + + public static GroupId from(String id) { + String[] pair = id.split(":", 2); + return from(TenantId.from(pair[0]), pair[1]); + } + + private GroupId(final TenantId tenantId, final String groupName) { + this.tenantId = tenantId; + this.groupName = groupName; + } + + public String idString() { + return tenantId.id != "" || groupName != "" ? String.format("%s:%s", tenantId.id, groupName) : ""; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof GroupId)) { + return false; + } + GroupId groupId = (GroupId) other; + return tenantId.equals(groupId.tenantId) && groupName.equals(groupId.groupName); + } + + @Override + public int hashCode() { + return Objects.hash(tenantId, groupName); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("tenantId", tenantId.idString()) + .append("groupName", groupName) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupProvisioned.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupProvisioned.java new file mode 100644 index 00000000..84bd708f --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupProvisioned.java @@ -0,0 +1,30 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class GroupProvisioned extends IdentifiedDomainEvent { + + public final GroupId groupId; + public final String name; + public final String description; + + public GroupProvisioned(final GroupId groupId, final String name, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.groupId = groupId; + this.name = name; + this.description = description; + } + + @Override + public String identity() { + return groupId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupState.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupState.java new file mode 100644 index 00000000..ffefc88d --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupState.java @@ -0,0 +1,69 @@ +package io.vlingo.xoom.auth.model.group; + + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.value.EncodedMember; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class GroupState { + + public final GroupId id; + public final String name; + public final String description; + public final Set members; + + public static GroupState identifiedBy(final GroupId groupId) { + return new GroupState(groupId, null, null, Collections.emptySet()); + } + + public GroupState(final GroupId groupId, final String name, final String description) { + this(groupId, name, description, Collections.emptySet()); + } + + private GroupState(final GroupId groupId, final String name, final String description, final Set members) { + this.id = groupId; + this.name = name; + this.description = description; + this.members = Collections.unmodifiableSet(members); + } + + public GroupState provisionGroup(final String name, final String description) { + return new GroupState(this.id, name, description, Collections.emptySet()); + } + + public GroupState changeDescription(final String description) { + return new GroupState(this.id, this.name, description, members); + } + + public GroupState assignGroup(final GroupId innerGroupId) { + final Set updatedMembers = includeMember(EncodedMember.group(innerGroupId)); + return new GroupState(this.id, this.name, this.description, updatedMembers); + } + + public GroupState unassignGroup(final GroupId innerGroupId) { + final Set updatedMembers = removeMember(EncodedMember.group(innerGroupId)); + return new GroupState(this.id, this.name, this.description, updatedMembers); + } + + public GroupState assignUser(final UserId userId) { + final Set updatedMembers = includeMember(EncodedMember.user(userId)); + return new GroupState(this.id, this.name, this.description, updatedMembers); + } + + public GroupState unassignUser(final UserId userId) { + final Set updatedMembers = removeMember(EncodedMember.user(userId)); + return new GroupState(this.id, this.name, this.description, updatedMembers); + } + + private Set includeMember(final EncodedMember member) { + return Stream.concat(members.stream(), Stream.of(member)).collect(Collectors.toSet()); + } + + private Set removeMember(final EncodedMember member) { + return members.stream().filter(m -> !m.equals(member)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupUnassignedFromGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupUnassignedFromGroup.java new file mode 100644 index 00000000..6c264229 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupUnassignedFromGroup.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class GroupUnassignedFromGroup extends IdentifiedDomainEvent { + + public final GroupId groupId; + public final GroupId innerGroupId; + + public GroupUnassignedFromGroup(final GroupId groupId, final GroupId innerGroupId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.groupId = groupId; + this.innerGroupId = innerGroupId; + } + + @Override + public String identity() { + return groupId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/UserAssignedToGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/UserAssignedToGroup.java new file mode 100644 index 00000000..081b50be --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/UserAssignedToGroup.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserAssignedToGroup extends IdentifiedDomainEvent { + + public final GroupId groupId; + public final UserId userId; + + public UserAssignedToGroup(final GroupId groupId, final UserId userId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.groupId = groupId; + this.userId = userId; + } + + @Override + public String identity() { + return groupId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/UserUnassignedFromGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/UserUnassignedFromGroup.java new file mode 100644 index 00000000..08337fa5 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/group/UserUnassignedFromGroup.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserUnassignedFromGroup extends IdentifiedDomainEvent { + + public final GroupId groupId; + public final UserId userId; + + public UserUnassignedFromGroup(final GroupId groupId, final UserId userId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.groupId = groupId; + this.userId = userId; + } + + @Override + public String identity() { + return groupId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/Permission.java b/src/main/java/io/vlingo/xoom/auth/model/permission/Permission.java new file mode 100644 index 00000000..59163bd3 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/Permission.java @@ -0,0 +1,18 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.auth.model.value.Constraint; +import io.vlingo.xoom.common.Completes; + +public interface Permission { + + Completes provisionPermission(final String name, final String description); + + Completes enforce(final Constraint constraint); + + Completes enforceReplacement(String constraintName, final Constraint constraint); + + Completes forget(final String constraintName); + + Completes changeDescription(final String description); + +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintEnforced.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintEnforced.java new file mode 100644 index 00000000..b1b0a3c7 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintEnforced.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +import io.vlingo.xoom.auth.model.value.*; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class PermissionConstraintEnforced extends IdentifiedDomainEvent { + + public final PermissionId permissionId; + public final Constraint constraint; + + public PermissionConstraintEnforced(final PermissionId permissionId, final Constraint constraint) { + super(SemanticVersion.from("1.0.0").toValue()); + this.permissionId = permissionId; + this.constraint = constraint; + } + + @Override + public String identity() { + return permissionId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintForgotten.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintForgotten.java new file mode 100644 index 00000000..a108ad80 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintForgotten.java @@ -0,0 +1,27 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class PermissionConstraintForgotten extends IdentifiedDomainEvent { + + public final PermissionId permissionId; + public final String constraintName; + + public PermissionConstraintForgotten(final PermissionId permissionId, final String constraintName) { + super(SemanticVersion.from("1.0.0").toValue()); + this.permissionId = permissionId; + this.constraintName = constraintName; + } + + @Override + public String identity() { + return permissionId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintReplacementEnforced.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintReplacementEnforced.java new file mode 100644 index 00000000..d1719a07 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintReplacementEnforced.java @@ -0,0 +1,31 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +import io.vlingo.xoom.auth.model.value.*; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class PermissionConstraintReplacementEnforced extends IdentifiedDomainEvent { + + public final PermissionId permissionId; + public final String constraintName; + public final Constraint constraint; + + public PermissionConstraintReplacementEnforced(final PermissionId permissionId, final String constraintName, final Constraint constraint) { + super(SemanticVersion.from("1.0.0").toValue()); + this.permissionId = permissionId; + this.constraintName = constraintName; + this.constraint = constraint; + } + + @Override + public String identity() { + return permissionId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionDescriptionChanged.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionDescriptionChanged.java new file mode 100644 index 00000000..8d19c4b9 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionDescriptionChanged.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class PermissionDescriptionChanged extends IdentifiedDomainEvent { + + public final PermissionId permissionId; + public final String description; + + public PermissionDescriptionChanged(final PermissionId permissionId, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.permissionId = permissionId; + this.description = description; + } + + @Override + public String identity() { + return permissionId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionEntity.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionEntity.java new file mode 100644 index 00000000..939bfc86 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionEntity.java @@ -0,0 +1,97 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.auth.model.value.Constraint; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.EventSourced; + +/** + * See EventSourced + */ +public final class PermissionEntity extends EventSourced implements Permission { + private PermissionState state; + + public PermissionEntity(final PermissionId permissionId) { + super(permissionId.idString()); + this.state = PermissionState.identifiedBy(permissionId); + } + + static { + EventSourced.registerConsumer(PermissionEntity.class, PermissionProvisioned.class, PermissionEntity::applyPermissionProvisioned); + EventSourced.registerConsumer(PermissionEntity.class, PermissionConstraintEnforced.class, PermissionEntity::applyPermissionConstraintEnforced); + EventSourced.registerConsumer(PermissionEntity.class, PermissionConstraintReplacementEnforced.class, PermissionEntity::applyPermissionConstraintReplacementEnforced); + EventSourced.registerConsumer(PermissionEntity.class, PermissionConstraintForgotten.class, PermissionEntity::applyPermissionConstraintForgotten); + EventSourced.registerConsumer(PermissionEntity.class, PermissionDescriptionChanged.class, PermissionEntity::applyPermissionDescriptionChanged); + } + + @Override + public Completes provisionPermission(final String name, final String description) { + return apply(new PermissionProvisioned(state.id, name, description), () -> state); + } + + @Override + public Completes enforce(final Constraint constraint) { + return apply(new PermissionConstraintEnforced(state.id, constraint), () -> state); + } + + @Override + public Completes enforceReplacement(final String constraintName, final Constraint constraint) { + return apply(new PermissionConstraintReplacementEnforced(state.id, constraintName, constraint), () -> state); + } + + @Override + public Completes forget(final String constraintName) { + return apply(new PermissionConstraintForgotten(state.id, constraintName), () -> state); + } + + @Override + public Completes changeDescription(final String description) { + return apply(new PermissionDescriptionChanged(state.id, description), () -> state); + } + + private void applyPermissionProvisioned(final PermissionProvisioned event) { + state = state.provisionPermission(event.name, event.description); + } + + private void applyPermissionConstraintEnforced(final PermissionConstraintEnforced event) { + state = state.enforce(event.constraint); + } + + private void applyPermissionConstraintReplacementEnforced(final PermissionConstraintReplacementEnforced event) { + state = state.enforceReplacement(event.constraintName, event.constraint); + } + + private void applyPermissionConstraintForgotten(final PermissionConstraintForgotten event) { + state = state.forget(event.constraintName); + } + + private void applyPermissionDescriptionChanged(final PermissionDescriptionChanged event) { + state = state.changeDescription(event.description); + } + + /* + * Restores my initial state by means of {@code state}. + * + * @param snapshot the {@code PermissionState} holding my state + * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed + */ + @Override + @SuppressWarnings("hiding") + protected void restoreSnapshot(final PermissionState snapshot, final int currentVersion) { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + } + + /* + * Answer the valid {@code PermissionState} instance if a snapshot should + * be taken and persisted along with applied {@code Source} instance(s). + * + * @return PermissionState + */ + @Override + @SuppressWarnings("unchecked") + protected PermissionState snapshot() { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + return null; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionId.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionId.java new file mode 100644 index 00000000..12329e08 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionId.java @@ -0,0 +1,50 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Objects; + +public class PermissionId { + public final TenantId tenantId; + public final String permissionName; + + public static PermissionId from(final TenantId tenantId, final String permissionName) { + return new PermissionId(tenantId, permissionName); + } + + private PermissionId(final TenantId tenantId, final String permissionName) { + this.tenantId = tenantId; + this.permissionName = permissionName; + } + + public String idString() { + return tenantId.id != "" || permissionName != "" ? String.format("%s:%s", tenantId.idString(), permissionName) : ""; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PermissionId)) { + return false; + } + PermissionId permissionName = (PermissionId) other; + return tenantId.equals(permissionName.tenantId) && this.permissionName.equals(permissionName.permissionName); + } + + @Override + public int hashCode() { + return Objects.hash(tenantId, permissionName); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("tenantId", tenantId) + .append("permissionName", permissionName) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionProvisioned.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionProvisioned.java new file mode 100644 index 00000000..879a7ade --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionProvisioned.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class PermissionProvisioned extends IdentifiedDomainEvent { + + public final PermissionId permissionId; + public final String name; + public final String description; + + public PermissionProvisioned(final PermissionId permissionId, final String name, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.permissionId = permissionId; + this.name = name; + this.description = description; + } + + @Override + public String identity() { + return permissionId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionState.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionState.java new file mode 100644 index 00000000..8a5eab99 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionState.java @@ -0,0 +1,70 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.auth.model.value.Constraint; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class PermissionState { + + public final PermissionId id; + public final Set constraints; + public final String name; + public final String description; + + public static PermissionState identifiedBy(final PermissionId permissionId) { + return new PermissionState(permissionId, new HashSet<>(), null, null); + } + + public PermissionState(final PermissionId permissionId, final Set constraints, final String name, final String description) { + this.id = permissionId; + this.constraints = Collections.unmodifiableSet(constraints); + this.name = name; + this.description = description; + } + + public PermissionState provisionPermission(final String name, final String description) { + return new PermissionState(this.id, this.constraints, name, description); + } + + public PermissionState enforce(final Constraint constraint) { + return new PermissionState(this.id, includeConstraint(constraints, constraint), this.name, this.description); + } + + public PermissionState enforceReplacement(final String constraintName, final Constraint constraint) { + Set updatedConstraints = constraintOf(constraintName) + .map(c -> removeConstraint(this.constraints, c)) + .map(c -> includeConstraint(c, constraint)) + .orElse(this.constraints); + return new PermissionState(this.id, updatedConstraints, this.name, this.description); + } + + public PermissionState forget(final String constraintName) { + Set updatedConstraints = constraintOf(constraintName) + .map(c -> removeConstraint(this.constraints, c)) + .orElse(this.constraints); + return new PermissionState(this.id, updatedConstraints, this.name, this.description); + } + + public PermissionState changeDescription(final String description) { + return new PermissionState(this.id, this.constraints, this.name, description); + } + + private Optional constraintOf(final String constraintName) { + return this.constraints.stream() + .filter(c -> c.name.equals(constraintName)) + .findFirst(); + } + + private Set includeConstraint(final Set constraints, final Constraint constraint) { + return Stream.concat(constraints.stream(), Stream.of(constraint)).collect(Collectors.toSet()); + } + + private Set removeConstraint(final Set constraints, final Constraint constraint) { + return constraints.stream().filter(c -> !c.equals(constraint)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/GroupAssignedToRole.java b/src/main/java/io/vlingo/xoom/auth/model/role/GroupAssignedToRole.java new file mode 100644 index 00000000..e0e24d8a --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/GroupAssignedToRole.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class GroupAssignedToRole extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final GroupId groupId; + + public GroupAssignedToRole(final RoleId roleId, final GroupId groupId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.groupId = groupId; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/GroupUnassignedFromRole.java b/src/main/java/io/vlingo/xoom/auth/model/role/GroupUnassignedFromRole.java new file mode 100644 index 00000000..76a12c80 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/GroupUnassignedFromRole.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class GroupUnassignedFromRole extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final GroupId groupId; + + public GroupUnassignedFromRole(final RoleId roleId, final GroupId groupId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.groupId = groupId; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/Role.java b/src/main/java/io/vlingo/xoom/auth/model/role/Role.java new file mode 100644 index 00000000..fc448ec7 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/Role.java @@ -0,0 +1,26 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; + +public interface Role { + + Completes provisionRole(final String name, final String description); + + Completes changeDescription(final String description); + + Completes assignGroup(final GroupId groupId); + + Completes unassignGroup(final GroupId groupId); + + Completes assignUser(final UserId userId); + + Completes unassignUser(final UserId userId); + + Completes attach(final PermissionId permissionId); + + Completes detach(final PermissionId permissionId); + +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RoleDescriptionChanged.java b/src/main/java/io/vlingo/xoom/auth/model/role/RoleDescriptionChanged.java new file mode 100644 index 00000000..74b510d3 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RoleDescriptionChanged.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class RoleDescriptionChanged extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final String description; + + public RoleDescriptionChanged(final RoleId roleId, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.description = description; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RoleEntity.java b/src/main/java/io/vlingo/xoom/auth/model/role/RoleEntity.java new file mode 100644 index 00000000..7288b13b --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RoleEntity.java @@ -0,0 +1,153 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.EventSourced; + +/** + * See EventSourced + */ +public final class RoleEntity extends EventSourced implements Role { + private RoleState state; + + public RoleEntity(final RoleId roleId) { + super(roleId.idString()); + this.state = RoleState.identifiedBy(roleId); + } + + static { + EventSourced.registerConsumer(RoleEntity.class, RoleProvisioned.class, RoleEntity::applyRoleProvisioned); + EventSourced.registerConsumer(RoleEntity.class, RoleDescriptionChanged.class, RoleEntity::applyRoleDescriptionChanged); + EventSourced.registerConsumer(RoleEntity.class, GroupAssignedToRole.class, RoleEntity::applyGroupAssignedToRole); + EventSourced.registerConsumer(RoleEntity.class, GroupUnassignedFromRole.class, RoleEntity::applyGroupUnassignedFromRole); + EventSourced.registerConsumer(RoleEntity.class, UserAssignedToRole.class, RoleEntity::applyUserAssignedToRole); + EventSourced.registerConsumer(RoleEntity.class, UserUnassignedFromRole.class, RoleEntity::applyUserUnassignedFromRole); + EventSourced.registerConsumer(RoleEntity.class, RolePermissionAttached.class, RoleEntity::applyRolePermissionAttached); + EventSourced.registerConsumer(RoleEntity.class, RolePermissionDetached.class, RoleEntity::applyRolePermissionDetached); + } + + @Override + public Completes provisionRole(final String name, final String description) { + /** + * TODO: Implement command logic. See {@link RoleState#provisionRole()} + */ + return apply(new RoleProvisioned(state.roleId, name, description), () -> state); + } + + @Override + public Completes changeDescription(final String description) { + /** + * TODO: Implement command logic. See {@link RoleState#changeDescription()} + */ + return apply(new RoleDescriptionChanged(state.roleId, description), () -> state); + } + + @Override + public Completes assignGroup(final GroupId groupId) { + /** + * TODO: Implement command logic. See {@link RoleState#assignGroup()} + */ + return apply(new GroupAssignedToRole(state.roleId, groupId), () -> state); + } + + @Override + public Completes unassignGroup(final GroupId groupId) { + /** + * TODO: Implement command logic. See {@link RoleState#unassignGroup()} + */ + return apply(new GroupUnassignedFromRole(state.roleId, groupId), () -> state); + } + + @Override + public Completes assignUser(final UserId userId) { + /** + * TODO: Implement command logic. See {@link RoleState#assignUser()} + */ + return apply(new UserAssignedToRole(state.roleId, userId), () -> state); + } + + @Override + public Completes unassignUser(final UserId userId) { + /** + * TODO: Implement command logic. See {@link RoleState#unassignUser()} + */ + return apply(new UserUnassignedFromRole(state.roleId, userId), () -> state); + } + + @Override + public Completes attach(final PermissionId permissionId) { + /** + * TODO: Implement command logic. See {@link RoleState#attach()} + */ + return apply(new RolePermissionAttached(state.roleId, permissionId), () -> state); + } + + @Override + public Completes detach(final PermissionId permissionId) { + /** + * TODO: Implement command logic. See {@link RoleState#detach()} + */ + return apply(new RolePermissionDetached(state.roleId, permissionId), () -> state); + } + + private void applyRoleProvisioned(final RoleProvisioned event) { + state = state.provisionRole(event.name, event.description); + } + + private void applyRoleDescriptionChanged(final RoleDescriptionChanged event) { + state = state.changeDescription(event.description); + } + + private void applyGroupAssignedToRole(final GroupAssignedToRole event) { + state = state.assignGroup(event.groupId); + } + + private void applyGroupUnassignedFromRole(final GroupUnassignedFromRole event) { + state = state.unassignGroup(event.groupId); + } + + private void applyUserAssignedToRole(final UserAssignedToRole event) { + state = state.assignUser(event.userId); + } + + private void applyUserUnassignedFromRole(final UserUnassignedFromRole event) { + state = state.unassignUser(event.userId); + } + + private void applyRolePermissionAttached(final RolePermissionAttached event) { + state = state.attach(event.permissionId); + } + + private void applyRolePermissionDetached(final RolePermissionDetached event) { + state = state.detach(event.permissionId); + } + + /* + * Restores my initial state by means of {@code state}. + * + * @param snapshot the {@code RoleState} holding my state + * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed + */ + @Override + @SuppressWarnings("hiding") + protected void restoreSnapshot(final RoleState snapshot, final int currentVersion) { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + } + + /* + * Answer the valid {@code RoleState} instance if a snapshot should + * be taken and persisted along with applied {@code Source} instance(s). + * + * @return RoleState + */ + @Override + @SuppressWarnings("unchecked") + protected RoleState snapshot() { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + return null; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RoleId.java b/src/main/java/io/vlingo/xoom/auth/model/role/RoleId.java new file mode 100644 index 00000000..18195ba3 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RoleId.java @@ -0,0 +1,50 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Objects; + +public class RoleId { + public final TenantId tenantId; + public final String roleName; + + public static RoleId from(final TenantId tenantId, final String roleName) { + return new RoleId(tenantId, roleName); + } + + private RoleId(final TenantId tenantId, final String roleName) { + this.tenantId = tenantId; + this.roleName = roleName; + } + + public String idString() { + return tenantId.id != "" || roleName != "" ? String.format("%s:%s", tenantId.id, roleName) : ""; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof RoleId)) { + return false; + } + RoleId otherRoleId = (RoleId) other; + return tenantId.equals(otherRoleId.tenantId) && this.roleName.equals(otherRoleId.roleName); + } + + @Override + public int hashCode() { + return Objects.hash(tenantId, roleName); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("tenantId", tenantId) + .append("roleName", roleName) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RolePermissionAttached.java b/src/main/java/io/vlingo/xoom/auth/model/role/RolePermissionAttached.java new file mode 100644 index 00000000..baccf27f --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RolePermissionAttached.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class RolePermissionAttached extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final PermissionId permissionId; + + public RolePermissionAttached(final RoleId roleId, final PermissionId permissionId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.permissionId = permissionId; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RolePermissionDetached.java b/src/main/java/io/vlingo/xoom/auth/model/role/RolePermissionDetached.java new file mode 100644 index 00000000..b60aef5b --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RolePermissionDetached.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class RolePermissionDetached extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final PermissionId permissionId; + + public RolePermissionDetached(final RoleId roleId, final PermissionId permissionId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.permissionId = permissionId; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RoleProvisioned.java b/src/main/java/io/vlingo/xoom/auth/model/role/RoleProvisioned.java new file mode 100644 index 00000000..316ba6cd --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RoleProvisioned.java @@ -0,0 +1,30 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class RoleProvisioned extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final String name; + public final String description; + + public RoleProvisioned(final RoleId roleId, final String name, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.name = name; + this.description = description; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/RoleState.java b/src/main/java/io/vlingo/xoom/auth/model/role/RoleState.java new file mode 100644 index 00000000..4d3a1193 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/RoleState.java @@ -0,0 +1,64 @@ +package io.vlingo.xoom.auth.model.role; + + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.user.UserId; + +public final class RoleState { + + public final RoleId roleId; + public final String name; + public final String description; + + public static RoleState identifiedBy(final RoleId roleId) { + return new RoleState(roleId, null, null); + } + + public RoleState(final RoleId roleId, final String name, final String description) { + this.roleId = roleId; + this.name = name; + this.description = description; + } + + public RoleState provisionRole(final String name, final String description) { + //TODO: Implement command logic. + return new RoleState(this.roleId, name, description); + } + + public RoleState changeDescription(final String description) { + //TODO: Implement command logic. + return new RoleState(this.roleId, this.name, description); + } + + public RoleState assignGroup(final GroupId groupId) { + //TODO: Implement command logic. + return new RoleState(this.roleId,this.name, this.description); + } + + public RoleState unassignGroup(final GroupId groupId) { + //TODO: Implement command logic. + return new RoleState(this.roleId, this.name, this.description); + } + + public RoleState assignUser(final UserId userId) { + //TODO: Implement command logic. + return new RoleState(this.roleId, this.name, this.description); + } + + public RoleState unassignUser(final UserId userId) { + //TODO: Implement command logic. + return new RoleState(this.roleId, this.name, this.description); + } + + public RoleState attach(final PermissionId permissionId) { + //TODO: Implement command logic. + return new RoleState(this.roleId, this.name, this.description); + } + + public RoleState detach(final PermissionId permissionId) { + //TODO: Implement command logic. + return new RoleState(this.roleId, this.name, this.description); + } + +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/UserAssignedToRole.java b/src/main/java/io/vlingo/xoom/auth/model/role/UserAssignedToRole.java new file mode 100644 index 00000000..f2bbf537 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/UserAssignedToRole.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserAssignedToRole extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final UserId userId; + + public UserAssignedToRole(final RoleId roleId, final UserId userId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.userId = userId; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/role/UserUnassignedFromRole.java b/src/main/java/io/vlingo/xoom/auth/model/role/UserUnassignedFromRole.java new file mode 100644 index 00000000..c43aa6ba --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/role/UserUnassignedFromRole.java @@ -0,0 +1,29 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserUnassignedFromRole extends IdentifiedDomainEvent { + + public final RoleId roleId; + public final UserId userId; + + public UserUnassignedFromRole(final RoleId roleId, final UserId userId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.roleId = roleId; + this.userId = userId; + } + + @Override + public String identity() { + return roleId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/Tenant.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/Tenant.java new file mode 100644 index 00000000..84fe073d --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/Tenant.java @@ -0,0 +1,17 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.Completes; + +public interface Tenant { + + Completes subscribeFor(final String name, final String description, final boolean active); + + Completes activate(); + + Completes deactivate(); + + Completes changeName(final String name); + + Completes changeDescription(final String description); + +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantActivated.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantActivated.java new file mode 100644 index 00000000..4833bcc1 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantActivated.java @@ -0,0 +1,26 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class TenantActivated extends IdentifiedDomainEvent { + + public final TenantId tenantId; + + public TenantActivated(final TenantId tenantId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.tenantId = tenantId; + } + + @Override + public String identity() { + return tenantId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantDeactivated.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantDeactivated.java new file mode 100644 index 00000000..93eff021 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantDeactivated.java @@ -0,0 +1,26 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class TenantDeactivated extends IdentifiedDomainEvent { + + public final TenantId tenantId; + + public TenantDeactivated(final TenantId tenantId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.tenantId = tenantId; + } + + @Override + public String identity() { + return tenantId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantDescriptionChanged.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantDescriptionChanged.java new file mode 100644 index 00000000..91343c4a --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantDescriptionChanged.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class TenantDescriptionChanged extends IdentifiedDomainEvent { + + public final TenantId tenantId; + public final String description; + + public TenantDescriptionChanged(final TenantId tenantId, final String description) { + super(SemanticVersion.from("1.0.0").toValue()); + this.tenantId = tenantId; + this.description = description; + } + + @Override + public String identity() { + return tenantId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantEntity.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantEntity.java new file mode 100644 index 00000000..825f13ed --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantEntity.java @@ -0,0 +1,96 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.EventSourced; + +/** + * See EventSourced + */ +public final class TenantEntity extends EventSourced implements Tenant { + private TenantState state; + + public TenantEntity(final TenantId tenantId) { + super(tenantId.idString()); + this.state = TenantState.identifiedBy(tenantId); + } + + static { + EventSourced.registerConsumer(TenantEntity.class, TenantSubscribed.class, TenantEntity::applyTenantSubscribed); + EventSourced.registerConsumer(TenantEntity.class, TenantActivated.class, TenantEntity::applyTenantActivated); + EventSourced.registerConsumer(TenantEntity.class, TenantDeactivated.class, TenantEntity::applyTenantDeactivated); + EventSourced.registerConsumer(TenantEntity.class, TenantNameChanged.class, TenantEntity::applyTenantNameChanged); + EventSourced.registerConsumer(TenantEntity.class, TenantDescriptionChanged.class, TenantEntity::applyTenantDescriptionChanged); + } + + @Override + public Completes subscribeFor(final String name, final String description, final boolean active) { + return apply(new TenantSubscribed(state.tenantId, name, description, active), () -> state); + } + + @Override + public Completes activate() { + return apply(new TenantActivated(state.tenantId), () -> state); + } + + @Override + public Completes deactivate() { + return apply(new TenantDeactivated(state.tenantId), () -> state); + } + + @Override + public Completes changeName(final String name) { + return apply(new TenantNameChanged(state.tenantId, name), () -> state); + } + + @Override + public Completes changeDescription(final String description) { + return apply(new TenantDescriptionChanged(state.tenantId, description), () -> state); + } + + private void applyTenantSubscribed(final TenantSubscribed event) { + state = state.subscribeFor(event.name, event.description, event.active); + } + + private void applyTenantActivated(final TenantActivated event) { + state = state.activate(); + } + + private void applyTenantDeactivated(final TenantDeactivated event) { + state = state.deactivate(); + } + + private void applyTenantNameChanged(final TenantNameChanged event) { + state = state.changeName(event.name); + } + + private void applyTenantDescriptionChanged(final TenantDescriptionChanged event) { + state = state.changeDescription(event.description); + } + + /* + * Restores my initial state by means of {@code state}. + * + * @param snapshot the {@code TenantState} holding my state + * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed + */ + @Override + @SuppressWarnings("hiding") + protected void restoreSnapshot(final TenantState snapshot, final int currentVersion) { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + } + + /* + * Answer the valid {@code TenantState} instance if a snapshot should + * be taken and persisted along with applied {@code Source} instance(s). + * + * @return TenantState + */ + @Override + @SuppressWarnings("unchecked") + protected TenantState snapshot() { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + return null; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantId.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantId.java new file mode 100644 index 00000000..462fe7ef --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantId.java @@ -0,0 +1,50 @@ +package io.vlingo.xoom.auth.model.tenant; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Objects; +import java.util.UUID; + +public class TenantId { + public final String id; + + public static TenantId from(final String tenantId) { + return new TenantId(tenantId); + } + + public static TenantId unique() { + return new TenantId(UUID.randomUUID().toString()); + } + + private TenantId(final String id) { + this.id = id; + } + + public String idString() { + return id; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TenantId)) { + return false; + } + return id.equals(((TenantId) other).id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("id", id) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantNameChanged.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantNameChanged.java new file mode 100644 index 00000000..bc176385 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantNameChanged.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class TenantNameChanged extends IdentifiedDomainEvent { + + public final TenantId tenantId; + public final String name; + + public TenantNameChanged(final TenantId tenantId, final String name) { + super(SemanticVersion.from("1.0.0").toValue()); + this.tenantId = tenantId; + this.name = name; + } + + @Override + public String identity() { + return tenantId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantState.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantState.java new file mode 100644 index 00000000..d1dc1806 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantState.java @@ -0,0 +1,41 @@ +package io.vlingo.xoom.auth.model.tenant; + + +public final class TenantState { + + public final TenantId tenantId; + public final String name; + public final String description; + public final boolean active; + + public static TenantState identifiedBy(final TenantId tenantId) { + return new TenantState(tenantId, null, null, false); + } + + public TenantState(final TenantId tenantId, final String name, final String description, final boolean active) { + this.tenantId = tenantId; + this.name = name; + this.description = description; + this.active = active; + } + + public TenantState subscribeFor(final String name, final String description, final boolean active) { + return new TenantState(this.tenantId, name, description, active); + } + + public TenantState activate() { + return new TenantState(this.tenantId, this.name, this.description, true); + } + + public TenantState deactivate() { + return new TenantState(this.tenantId, this.name, this.description, false); + } + + public TenantState changeName(final String name) { + return new TenantState(this.tenantId, name, this.description, this.active); + } + + public TenantState changeDescription(final String description) { + return new TenantState(this.tenantId, this.name, description, this.active); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantSubscribed.java b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantSubscribed.java new file mode 100644 index 00000000..859eb91e --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/tenant/TenantSubscribed.java @@ -0,0 +1,32 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class TenantSubscribed extends IdentifiedDomainEvent { + + public final TenantId tenantId; + public final String name; + public final String description; + public final boolean active; + + public TenantSubscribed(final TenantId tenantId, final String name, final String description, final boolean active) { + super(SemanticVersion.from("1.0.0").toValue()); + this.tenantId = tenantId; + this.name = name; + this.description = description; + this.active = active; + } + + @Override + public String identity() { + return tenantId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/User.java b/src/main/java/io/vlingo/xoom/auth/model/user/User.java new file mode 100644 index 00000000..0faac77d --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/User.java @@ -0,0 +1,25 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.Completes; + +import java.util.Set; + +public interface User { + + Completes registerUser(final String username, final Profile profile, final Set credentials, final boolean active); + + Completes activate(); + + Completes deactivate(); + + Completes addCredential(final Credential credential); + + Completes removeCredential(final String authority); + + Completes replaceCredential(final String authority, final Credential credential); + + Completes replaceProfile(final Profile profile); + +} \ No newline at end of file diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserActivated.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserActivated.java new file mode 100644 index 00000000..3852d29d --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserActivated.java @@ -0,0 +1,26 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserActivated extends IdentifiedDomainEvent { + + public final UserId userId; + + public UserActivated(final UserId userId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialAdded.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialAdded.java new file mode 100644 index 00000000..1418e5b2 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialAdded.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserCredentialAdded extends IdentifiedDomainEvent { + + public final UserId userId; + public final Credential credential; + + public UserCredentialAdded(final UserId userId, final Credential credential) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + this.credential = credential; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialRemoved.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialRemoved.java new file mode 100644 index 00000000..565663f9 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialRemoved.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserCredentialRemoved extends IdentifiedDomainEvent { + + public final UserId userId; + public final String authority; + + public UserCredentialRemoved(final UserId userId, final String authority) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + this.authority = authority; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialReplaced.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialReplaced.java new file mode 100644 index 00000000..b42366bb --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserCredentialReplaced.java @@ -0,0 +1,30 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserCredentialReplaced extends IdentifiedDomainEvent { + + public final UserId userId; + public final String authority; + public final Credential credential; + + public UserCredentialReplaced(final UserId userId, String authority, final Credential credential) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + this.authority = authority; + this.credential = credential; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserDeactivated.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserDeactivated.java new file mode 100644 index 00000000..60e2d2e7 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserDeactivated.java @@ -0,0 +1,26 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserDeactivated extends IdentifiedDomainEvent { + + public final UserId userId; + + public UserDeactivated(final UserId userId) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserEntity.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserEntity.java new file mode 100644 index 00000000..5d8065d9 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserEntity.java @@ -0,0 +1,120 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.EventSourced; + +import java.util.Set; + +/** + * See EventSourced + */ +public final class UserEntity extends EventSourced implements User { + private UserState state; + + public UserEntity(final UserId userId) { + super(userId.idString()); + this.state = UserState.identifiedBy(userId); + } + + static { + EventSourced.registerConsumer(UserEntity.class, UserRegistered.class, UserEntity::applyUserRegistered); + EventSourced.registerConsumer(UserEntity.class, UserActivated.class, UserEntity::applyUserActivated); + EventSourced.registerConsumer(UserEntity.class, UserDeactivated.class, UserEntity::applyUserDeactivated); + EventSourced.registerConsumer(UserEntity.class, UserCredentialAdded.class, UserEntity::applyUserCredentialAdded); + EventSourced.registerConsumer(UserEntity.class, UserCredentialRemoved.class, UserEntity::applyUserCredentialRemoved); + EventSourced.registerConsumer(UserEntity.class, UserCredentialReplaced.class, UserEntity::applyUserCredentialReplaced); + EventSourced.registerConsumer(UserEntity.class, UserProfileReplaced.class, UserEntity::applyUserProfileReplaced); + } + + @Override + public Completes registerUser(final String username, final Profile profile, final Set credentials, final boolean active) { + return apply(new UserRegistered(state.userId, username, profile, credentials, active), () -> state); + } + + @Override + public Completes activate() { + return apply(new UserActivated(state.userId), () -> state); + } + + @Override + public Completes deactivate() { + return apply(new UserDeactivated(state.userId), () -> state); + } + + @Override + public Completes addCredential(final Credential credential) { + return apply(new UserCredentialAdded(state.userId, credential), () -> state); + } + + @Override + public Completes removeCredential(final String authority) { + return apply(new UserCredentialRemoved(state.userId, authority), () -> state); + } + + @Override + public Completes replaceCredential(final String authority, final Credential credential) { + return apply(new UserCredentialReplaced(state.userId, authority, credential), () -> state); + } + + @Override + public Completes replaceProfile(final Profile profile) { + return apply(new UserProfileReplaced(state.userId, profile), () -> state); + } + + private void applyUserRegistered(final UserRegistered event) { + state = state.registerUser(event.username, event.profile, event.credentials, event.active); + } + + private void applyUserActivated(final UserActivated event) { + state = state.activate(); + } + + private void applyUserDeactivated(final UserDeactivated event) { + state = state.deactivate(); + } + + private void applyUserCredentialAdded(final UserCredentialAdded event) { + state = state.addCredential(event.credential); + } + + private void applyUserCredentialRemoved(final UserCredentialRemoved event) { + state = state.removeCredential(event.authority); + } + + private void applyUserCredentialReplaced(final UserCredentialReplaced event) { + state = state.replaceCredential(event.authority, event.credential); + } + + private void applyUserProfileReplaced(final UserProfileReplaced event) { + state = state.replaceProfile(event.profile); + } + + /* + * Restores my initial state by means of {@code state}. + * + * @param snapshot the {@code UserState} holding my state + * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed + */ + @Override + @SuppressWarnings("hiding") + protected void restoreSnapshot(final UserState snapshot, final int currentVersion) { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + } + + /* + * Answer the valid {@code UserState} instance if a snapshot should + * be taken and persisted along with applied {@code Source} instance(s). + * + * @return UserState + */ + @Override + @SuppressWarnings("unchecked") + protected UserState snapshot() { + // OVERRIDE FOR SNAPSHOT SUPPORT + // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced + return null; + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserId.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserId.java new file mode 100644 index 00000000..71f7c3b7 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserId.java @@ -0,0 +1,55 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Objects; + +public class UserId { + public final TenantId tenantId; + public final String username; + + public static UserId from(final TenantId tenantId, final String username) { + return new UserId(tenantId, username); + } + + public static UserId from(final String id) { + String[] pair = id.split(":", 2); + return from(TenantId.from(pair[0]), pair[1]); + } + + private UserId(final TenantId tenantId, final String username) { + this.tenantId = tenantId; + this.username = username; + } + + public String idString() { + return tenantId.id != "" || username != "" ? String.format("%s:%s", tenantId.id, username) : ""; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof UserId)) { + return false; + } + UserId otherUserId = (UserId) other; + return tenantId.equals(otherUserId.tenantId) && this.username.equals(otherUserId.username); + } + + @Override + public int hashCode() { + return Objects.hash(tenantId, username); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("tenantId", tenantId) + .append("username", username) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserProfileReplaced.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserProfileReplaced.java new file mode 100644 index 00000000..6a2672a9 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserProfileReplaced.java @@ -0,0 +1,28 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserProfileReplaced extends IdentifiedDomainEvent { + + public final UserId userId; + public final Profile profile; + + public UserProfileReplaced(final UserId userId, final Profile profile) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + this.profile = profile; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserRegistered.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserRegistered.java new file mode 100644 index 00000000..b957e004 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserRegistered.java @@ -0,0 +1,37 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.version.SemanticVersion; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; + +import java.util.Set; + +/** + * See + * + * Commands, Domain Events, and Identified Domain Events + * + */ +public final class UserRegistered extends IdentifiedDomainEvent { + + public final UserId userId; + public final String username; + public final boolean active; + public final Set credentials; + public final Profile profile; + + public UserRegistered(final UserId userId, final String username, final Profile profile, final Set credentials, final boolean active) { + super(SemanticVersion.from("1.0.0").toValue()); + this.userId = userId; + this.username = username; + this.active = active; + this.credentials = credentials; + this.profile = profile; + } + + @Override + public String identity() { + return userId.idString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/user/UserState.java b/src/main/java/io/vlingo/xoom/auth/model/user/UserState.java new file mode 100644 index 00000000..6097635c --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/user/UserState.java @@ -0,0 +1,66 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.Profile; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class UserState { + + public final UserId userId; + public final String username; + public final boolean active; + public final Set credentials; + public final Profile profile; + + public static UserState identifiedBy(final UserId userId) { + return new UserState(userId, null, null, Collections.emptySet(), false); + } + + public UserState(final UserId userId, final String username, final Profile profile, final Set credentials, final boolean active) { + this.userId = userId; + this.username = username; + this.active = active; + this.credentials = Collections.unmodifiableSet(credentials); + this.profile = profile; + } + + public UserState registerUser(final String username, final Profile profile, final Set credentials, final boolean active) { + return new UserState(this.userId, username, profile, credentials, active); + } + + public UserState activate() { + return new UserState(this.userId, this.username, this.profile, this.credentials, true); + } + + public UserState deactivate() { + return new UserState(this.userId, this.username, this.profile, this.credentials, false); + } + + public UserState addCredential(final Credential credential) { + return new UserState(this.userId, this.username, this.profile, includeCredential(this.credentials, credential), this.active); + } + + public UserState removeCredential(final String authority) { + return new UserState(this.userId, this.username, this.profile, removeCredential(this.credentials, authority), this.active); + } + + public UserState replaceCredential(final String authority, final Credential credential) { + return new UserState(this.userId, this.username, this.profile, includeCredential(removeCredential(this.credentials, authority), credential), this.active); + } + + public UserState replaceProfile(final Profile profile) { + return new UserState(this.userId, this.username, profile, this.credentials, this.active); + } + + private Set includeCredential(final Set credentials, final Credential credential) { + return Stream.concat(credentials.stream(), Stream.of(credential)).collect(Collectors.toSet()); + } + + private Set removeCredential(final Set credentials, final String authority) { + return credentials.stream().filter(c -> !c.authority.equals(authority)).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/value/Constraint.java b/src/main/java/io/vlingo/xoom/auth/model/value/Constraint.java new file mode 100644 index 00000000..2aa96cb5 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/value/Constraint.java @@ -0,0 +1,68 @@ +package io.vlingo.xoom.auth.model.value; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public final class Constraint { + public enum Type { String, Integer, Double, Boolean } + + public final String description; + public final String name; + public final Type type; + public final String value; + + @Deprecated + public static Constraint from(final String type, final String name, final String value, final String description) { + return new Constraint(Type.valueOf(type), name, value, description); + } + + public static Constraint from(final Type type, final String name, final String value, final String description) { + return new Constraint(type, name, value, description); + } + + private Constraint(final Type type, final String name, final String value, final String description) { + this.description = description; + this.name = name; + this.type = type; + this.value = value; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(description) + .append(name) + .append(type) + .append(value) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Constraint another = (Constraint) other; + return new EqualsBuilder() + .append(this.description, another.description) + .append(this.name, another.name) + .append(this.type, another.type) + .append(this.value, another.value) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("description", description) + .append("name", name) + .append("type", type) + .append("value", value) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/value/Credential.java b/src/main/java/io/vlingo/xoom/auth/model/value/Credential.java new file mode 100644 index 00000000..4e1ec9e3 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/value/Credential.java @@ -0,0 +1,85 @@ +package io.vlingo.xoom.auth.model.value; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public final class Credential { + + public enum Type { XOOM, OAUTH } + + public final String authority; + public final String id; + public final String secret; + public final Type type; + + public static Credential from(final String authority, final String id, final String secret, final String type) { + if (null == type || isXoomType(type)) { + return xoomCredentialFrom(authority, id, secret); + } else if (isOAuthType(type)) { + return oauthCredentialFrom(authority, id, secret); + } + throw new IllegalArgumentException(String.format("Unknown credential type: %s", type)); + } + + public static Credential xoomCredentialFrom(final String authority, final String id, final String secret) { + return new Credential(authority, id, secret, Type.XOOM); + } + + public static Credential oauthCredentialFrom(final String authority, final String id, final String secret) { + return new Credential(authority, id, secret, Type.OAUTH); + } + + private Credential(final String authority, final String id, final String secret, final Type type) { + this.authority = authority; + this.id = id; + this.secret = secret; + this.type = type; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(authority) + .append(id) + .append(secret) + .append(type) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Credential another = (Credential) other; + return new EqualsBuilder() + .append(this.authority, another.authority) + .append(this.id, another.id) + .append(this.secret, another.secret) + .append(this.type, another.type) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("authority", authority) + .append("id", id) + .append("secret", secret) + .append("type", type) + .toString(); + } + + private static boolean isOAuthType(final String credentialType) { + return Type.OAUTH.name().equals(credentialType); + } + + private static boolean isXoomType(final String credentialType) { + return Type.XOOM.name().equals(credentialType); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/value/EncodedMember.java b/src/main/java/io/vlingo/xoom/auth/model/value/EncodedMember.java new file mode 100644 index 00000000..b5270bb3 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/value/EncodedMember.java @@ -0,0 +1,69 @@ +package io.vlingo.xoom.auth.model.value; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.user.UserId; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Objects; + +public class EncodedMember { + static private final char GroupType = 'G'; + static private final char PermissionType = 'P'; + static private final char RoleType = 'R'; + static private final char UserType = 'U'; + + public final String id; + public final char type; + + + public static GroupMember group(GroupId groupId) { + return new GroupMember(groupId); + } + + public static UserMember user(UserId userId) { + return new UserMember(userId); + } + + private EncodedMember(final String id, final char type) { + this.id = id; + this.type = type; + } + + public static final class GroupMember extends EncodedMember { + private GroupMember(GroupId groupId) { + super(groupId.idString(), GroupType); + } + } + + public static final class UserMember extends EncodedMember { + private UserMember(UserId userId) { + super(userId.idString(), UserType); + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof EncodedMember)) { + return false; + } + EncodedMember member = (EncodedMember) other; + return type == member.type && id.equals(member.id); + } + + @Override + public int hashCode() { + return Objects.hash(id, type); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("type", type) + .append("id", id) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/value/PersonName.java b/src/main/java/io/vlingo/xoom/auth/model/value/PersonName.java new file mode 100644 index 00000000..c1f476f4 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/value/PersonName.java @@ -0,0 +1,57 @@ +package io.vlingo.xoom.auth.model.value; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public final class PersonName { + + public final String given; + public final String family; + public final String second; + + public static PersonName from(final String given, final String family, final String second) { + return new PersonName(given, family, second); + } + + private PersonName (final String given, final String family, final String second) { + this.given = given; + this.family = family; + this.second = second; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(given) + .append(family) + .append(second) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + PersonName another = (PersonName) other; + return new EqualsBuilder() + .append(this.given, another.given) + .append(this.family, another.family) + .append(this.second, another.second) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("given", given) + .append("family", family) + .append("second", second) + .toString(); + } +} diff --git a/src/main/java/io/vlingo/xoom/auth/model/value/Profile.java b/src/main/java/io/vlingo/xoom/auth/model/value/Profile.java new file mode 100644 index 00000000..92dfeb25 --- /dev/null +++ b/src/main/java/io/vlingo/xoom/auth/model/value/Profile.java @@ -0,0 +1,57 @@ +package io.vlingo.xoom.auth.model.value; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public final class Profile { + + public final String emailAddress; + public final PersonName name; + public final String phone; + + public static Profile from(final String emailAddress, final PersonName name, final String phone) { + return new Profile(emailAddress, name, phone); + } + + private Profile (final String emailAddress, final PersonName name, final String phone) { + this.emailAddress = emailAddress; + this.name = name; + this.phone = phone; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(31, 17) + .append(emailAddress) + .append(name) + .append(phone) + .toHashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Profile another = (Profile) other; + return new EqualsBuilder() + .append(this.emailAddress, another.emailAddress) + .append(this.name, another.name) + .append(this.phone, another.phone) + .isEquals(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE) + .append("emailAddress", emailAddress) + .append("name", name) + .append("phone", phone) + .toString(); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000..a65a9951 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/src/main/resources/xoom-actors.properties b/src/main/resources/xoom-actors.properties new file mode 100644 index 00000000..f9d998ec --- /dev/null +++ b/src/main/resources/xoom-actors.properties @@ -0,0 +1,21 @@ +plugin.name.queueMailbox = true +plugin.queueMailbox.classname = io.vlingo.xoom.actors.plugin.mailbox.concurrentqueue.ConcurrentQueueMailboxPlugin +plugin.queueMailbox.defaultMailbox = true +plugin.queueMailbox.numberOfDispatchersFactor = 1.5 +plugin.queueMailbox.numberOfDispatchers = 0 +plugin.queueMailbox.dispatcherThrottlingCount = 1 + +plugin.name.slf4jLogger = true +plugin.slf4jLogger.classname = io.vlingo.xoom.actors.plugin.logging.slf4j.Slf4jLoggerPlugin +plugin.slf4jLogger.name = XOOM +plugin.slf4jLogger.defaultLogger = true + +plugin.name.pooledCompletes = true +plugin.pooledCompletes.classname = io.vlingo.xoom.actors.plugin.completes.PooledCompletesPlugin +plugin.pooledCompletes.pool = 10 +plugin.pooledCompletes.mailbox = queueMailbox + +proxy.generated.classes.main = target/classes/ +proxy.generated.sources.main = target/generated-sources/ +proxy.generated.classes.test = target/test-classes/ +proxy.generated.sources.test = target/generated-test-sources/ \ No newline at end of file diff --git a/src/main/resources/xoom-cluster.properties b/src/main/resources/xoom-cluster.properties new file mode 100644 index 00000000..30df3ed2 --- /dev/null +++ b/src/main/resources/xoom-cluster.properties @@ -0,0 +1,80 @@ +################################ +# cluster-wide configurations +################################ + +# currently unsupported +cluster.ssl = false + +# maximum size of single operations message (which are actually tiny, other than DIR) +# assuming short host names 4096 would support approximately 90-99 nodes with DIR +cluster.op.buffer.size = 4096 + +# the interval (in ms) within which the operations inbound stream will be probed +# for available messages +cluster.op.incoming.probe.interval = 100 + +# maximum size of a single cluster client (tool or application) message +# you may be able to tune this to be much smaller depending on app messages +cluster.app.buffer.size = 10240 + +# the interval (in ms) within which the application inbound stream will be probed +# for available messages +cluster.app.incoming.probe.interval = 10 + +# number of polled buffers for outgoing asynchronous operations messages +cluster.op.outgoing.pooled.buffers = 20 + +# number of polled buffers for outgoing asynchronous operations messages +cluster.app.outgoing.pooled.buffers = 50 + +# default charset +cluster.msg.charset = UTF-8 + +# classname of client/application and its stage name +cluster.app.class = io.vlingo.lattice.grid.GridNode +cluster.app.stage = fake.app.stage + +# interval at which unconfirmed attribute changes are redistributed +cluster.attributes.redistribution.interval = 1000 + +# the number of retries for redistributing unconfirmed attribute changes +cluster.attributes.redistribution.retries = 20 + +# interval at which each health check is scheduled +cluster.health.check.interval = 2000 + +# after this limit with no pulse from given node, it's considered dead +cluster.live.node.timeout = 20000 + +# after this limit with too few nodes to constitute a quorum, terminate node +cluster.quorum.timeout = 60000 + +# currently all active nodes must be listed as seed nodes +# -- comment the following to disable developer single-node cluster +cluster.seedNodes = node1 + +# -- uncomment the following to enable all cluster nodes +# cluster.seedNodes = node1,node2,node3 + +################################ +# individual node configurations +################################ + +node.node1.id = 1 +node.node1.name = node1 +node.node1.host = localhost +node.node1.op.port = 38001 +node.node1.app.port = 38002 + +#node.node2.id = 2 +#node.node2.name = node2 +#node.node2.host = localhost +#node.node2.op.port = 38003 +#node.node2.app.port = 38004 + +#node.node3.id = 3 +#node.node3.name = node3 +#node.node3.host = localhost +#node.node3.op.port = 38005 +#node.node3.app.port = 38006 + diff --git a/src/main/resources/xoom-turbo.properties b/src/main/resources/xoom-turbo.properties new file mode 100644 index 00000000..288edb89 --- /dev/null +++ b/src/main/resources/xoom-turbo.properties @@ -0,0 +1,27 @@ +xoom.http.server.port=8080 + +xoom.lattice.exchange.local.port=37001 +database=IN_MEMORY +database.name=xoom-auth-command +database.driver= +database.url= +database.username= +database.password= +database.originator= + +query.database=IN_MEMORY +query.database.name=xoom-auth-query +query.database.driver= +query.database.url= +query.database.username= +query.database.password= +query.database.originator= + +exchange.names=xoom-auth-topic + +exchange.xoom-auth-topic.hostname=localhost +exchange.xoom-auth-topic.username=guest +exchange.xoom-auth-topic.password=guest +exchange.xoom-auth-topic.port=5672 +exchange.xoom-auth-topic.virtual.host=/ + diff --git a/src/main/vlingo/schemata/Constraint.vss b/src/main/vlingo/schemata/Constraint.vss new file mode 100644 index 00000000..75821a82 --- /dev/null +++ b/src/main/vlingo/schemata/Constraint.vss @@ -0,0 +1,7 @@ +data Constraint { + version semanticVersion + string description + string name + string type + string value +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/Credential.vss b/src/main/vlingo/schemata/Credential.vss new file mode 100644 index 00000000..284dc756 --- /dev/null +++ b/src/main/vlingo/schemata/Credential.vss @@ -0,0 +1,7 @@ +data Credential { + version semanticVersion + string authority + string id + string secret + string type +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/GroupAssignedToGroup.vss b/src/main/vlingo/schemata/GroupAssignedToGroup.vss new file mode 100644 index 00000000..63ccc504 --- /dev/null +++ b/src/main/vlingo/schemata/GroupAssignedToGroup.vss @@ -0,0 +1,5 @@ +event GroupAssignedToGroup { + version semanticVersion + string id + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/GroupAssignedToRole.vss b/src/main/vlingo/schemata/GroupAssignedToRole.vss new file mode 100644 index 00000000..0646eb79 --- /dev/null +++ b/src/main/vlingo/schemata/GroupAssignedToRole.vss @@ -0,0 +1,6 @@ +event GroupAssignedToRole { + version semanticVersion + string id + string tenantId + string name +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/GroupDescriptionChanged.vss b/src/main/vlingo/schemata/GroupDescriptionChanged.vss new file mode 100644 index 00000000..9bdb3df6 --- /dev/null +++ b/src/main/vlingo/schemata/GroupDescriptionChanged.vss @@ -0,0 +1,6 @@ +event GroupDescriptionChanged { + version semanticVersion + string id + string description + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/GroupProvisioned.vss b/src/main/vlingo/schemata/GroupProvisioned.vss new file mode 100644 index 00000000..1698a9aa --- /dev/null +++ b/src/main/vlingo/schemata/GroupProvisioned.vss @@ -0,0 +1,7 @@ +event GroupProvisioned { + version semanticVersion + string id + string name + string description + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/GroupUnassignedFromGroup.vss b/src/main/vlingo/schemata/GroupUnassignedFromGroup.vss new file mode 100644 index 00000000..334ff39f --- /dev/null +++ b/src/main/vlingo/schemata/GroupUnassignedFromGroup.vss @@ -0,0 +1,5 @@ +event GroupUnassignedFromGroup { + version semanticVersion + string id + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/GroupUnassignedFromRole.vss b/src/main/vlingo/schemata/GroupUnassignedFromRole.vss new file mode 100644 index 00000000..49e3820c --- /dev/null +++ b/src/main/vlingo/schemata/GroupUnassignedFromRole.vss @@ -0,0 +1,6 @@ +event GroupUnassignedFromRole { + version semanticVersion + string id + string tenantId + string name +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/PermissionConstraintEnforced.vss b/src/main/vlingo/schemata/PermissionConstraintEnforced.vss new file mode 100644 index 00000000..1ea9b05e --- /dev/null +++ b/src/main/vlingo/schemata/PermissionConstraintEnforced.vss @@ -0,0 +1,5 @@ +event PermissionConstraintEnforced { + version semanticVersion + string id + data.Constraint:1.0.0 constraints +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/PermissionConstraintForgotten.vss b/src/main/vlingo/schemata/PermissionConstraintForgotten.vss new file mode 100644 index 00000000..9f897bf5 --- /dev/null +++ b/src/main/vlingo/schemata/PermissionConstraintForgotten.vss @@ -0,0 +1,5 @@ +event PermissionConstraintForgotten { + version semanticVersion + string id + data.Constraint:1.0.0 constraints +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/PermissionConstraintReplacementEnforced.vss b/src/main/vlingo/schemata/PermissionConstraintReplacementEnforced.vss new file mode 100644 index 00000000..1cbd7350 --- /dev/null +++ b/src/main/vlingo/schemata/PermissionConstraintReplacementEnforced.vss @@ -0,0 +1,5 @@ +event PermissionConstraintReplacementEnforced { + version semanticVersion + string id + data.Constraint:1.0.0 constraints +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/PermissionDescriptionChanged.vss b/src/main/vlingo/schemata/PermissionDescriptionChanged.vss new file mode 100644 index 00000000..a91027f7 --- /dev/null +++ b/src/main/vlingo/schemata/PermissionDescriptionChanged.vss @@ -0,0 +1,6 @@ +event PermissionDescriptionChanged { + version semanticVersion + string id + string description + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/PermissionProvisioned.vss b/src/main/vlingo/schemata/PermissionProvisioned.vss new file mode 100644 index 00000000..30f729f1 --- /dev/null +++ b/src/main/vlingo/schemata/PermissionProvisioned.vss @@ -0,0 +1,7 @@ +event PermissionProvisioned { + version semanticVersion + string id + string description + string name + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/PersonName.vss b/src/main/vlingo/schemata/PersonName.vss new file mode 100644 index 00000000..14584464 --- /dev/null +++ b/src/main/vlingo/schemata/PersonName.vss @@ -0,0 +1,6 @@ +data PersonName { + version semanticVersion + string given + string family + string second +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/Profile.vss b/src/main/vlingo/schemata/Profile.vss new file mode 100644 index 00000000..61b93fce --- /dev/null +++ b/src/main/vlingo/schemata/Profile.vss @@ -0,0 +1,6 @@ +data Profile { + version semanticVersion + string emailAddress + data.PersonName:1.0.0 name + string phone +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/RoleDescriptionChanged.vss b/src/main/vlingo/schemata/RoleDescriptionChanged.vss new file mode 100644 index 00000000..bffd1dbf --- /dev/null +++ b/src/main/vlingo/schemata/RoleDescriptionChanged.vss @@ -0,0 +1,7 @@ +event RoleDescriptionChanged { + version semanticVersion + string id + string tenantId + string name + string description +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/RolePermissionAttached.vss b/src/main/vlingo/schemata/RolePermissionAttached.vss new file mode 100644 index 00000000..43e7993a --- /dev/null +++ b/src/main/vlingo/schemata/RolePermissionAttached.vss @@ -0,0 +1,6 @@ +event RolePermissionAttached { + version semanticVersion + string id + string tenantId + string name +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/RolePermissionDetached.vss b/src/main/vlingo/schemata/RolePermissionDetached.vss new file mode 100644 index 00000000..53c2c6df --- /dev/null +++ b/src/main/vlingo/schemata/RolePermissionDetached.vss @@ -0,0 +1,6 @@ +event RolePermissionDetached { + version semanticVersion + string id + string tenantId + string name +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/RoleProvisioned.vss b/src/main/vlingo/schemata/RoleProvisioned.vss new file mode 100644 index 00000000..339682fa --- /dev/null +++ b/src/main/vlingo/schemata/RoleProvisioned.vss @@ -0,0 +1,7 @@ +event RoleProvisioned { + version semanticVersion + string id + string tenantId + string name + string description +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/TenantActivated.vss b/src/main/vlingo/schemata/TenantActivated.vss new file mode 100644 index 00000000..835f06ae --- /dev/null +++ b/src/main/vlingo/schemata/TenantActivated.vss @@ -0,0 +1,4 @@ +event TenantActivated { + version semanticVersion + string id +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/TenantDeactivated.vss b/src/main/vlingo/schemata/TenantDeactivated.vss new file mode 100644 index 00000000..84bc3a55 --- /dev/null +++ b/src/main/vlingo/schemata/TenantDeactivated.vss @@ -0,0 +1,4 @@ +event TenantDeactivated { + version semanticVersion + string id +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/TenantDescriptionChanged.vss b/src/main/vlingo/schemata/TenantDescriptionChanged.vss new file mode 100644 index 00000000..8221c1de --- /dev/null +++ b/src/main/vlingo/schemata/TenantDescriptionChanged.vss @@ -0,0 +1,5 @@ +event TenantDescriptionChanged { + version semanticVersion + string id + string description +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/TenantNameChanged.vss b/src/main/vlingo/schemata/TenantNameChanged.vss new file mode 100644 index 00000000..10bbe8cd --- /dev/null +++ b/src/main/vlingo/schemata/TenantNameChanged.vss @@ -0,0 +1,5 @@ +event TenantNameChanged { + version semanticVersion + string id + string name +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/TenantSubscribed.vss b/src/main/vlingo/schemata/TenantSubscribed.vss new file mode 100644 index 00000000..2ca7e5a2 --- /dev/null +++ b/src/main/vlingo/schemata/TenantSubscribed.vss @@ -0,0 +1,7 @@ +event TenantSubscribed { + version semanticVersion + string id + string name + string description + boolean active +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserActivated.vss b/src/main/vlingo/schemata/UserActivated.vss new file mode 100644 index 00000000..0c484c05 --- /dev/null +++ b/src/main/vlingo/schemata/UserActivated.vss @@ -0,0 +1,6 @@ +event UserActivated { + version semanticVersion + string id + string tenantId + string username +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserAssignedToGroup.vss b/src/main/vlingo/schemata/UserAssignedToGroup.vss new file mode 100644 index 00000000..d474b24d --- /dev/null +++ b/src/main/vlingo/schemata/UserAssignedToGroup.vss @@ -0,0 +1,5 @@ +event UserAssignedToGroup { + version semanticVersion + string id + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserAssignedToRole.vss b/src/main/vlingo/schemata/UserAssignedToRole.vss new file mode 100644 index 00000000..15ee12b9 --- /dev/null +++ b/src/main/vlingo/schemata/UserAssignedToRole.vss @@ -0,0 +1,6 @@ +event UserAssignedToRole { + version semanticVersion + string id + string tenantId + string name +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserCredentialAdded.vss b/src/main/vlingo/schemata/UserCredentialAdded.vss new file mode 100644 index 00000000..63d8703f --- /dev/null +++ b/src/main/vlingo/schemata/UserCredentialAdded.vss @@ -0,0 +1,5 @@ +event UserCredentialAdded { + version semanticVersion + string id + data.Credential:1.0.0 credentials +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserCredentialRemoved.vss b/src/main/vlingo/schemata/UserCredentialRemoved.vss new file mode 100644 index 00000000..0a39e212 --- /dev/null +++ b/src/main/vlingo/schemata/UserCredentialRemoved.vss @@ -0,0 +1,5 @@ +event UserCredentialRemoved { + version semanticVersion + string id + data.Credential:1.0.0 credentials +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserCredentialReplaced.vss b/src/main/vlingo/schemata/UserCredentialReplaced.vss new file mode 100644 index 00000000..6d861312 --- /dev/null +++ b/src/main/vlingo/schemata/UserCredentialReplaced.vss @@ -0,0 +1,5 @@ +event UserCredentialReplaced { + version semanticVersion + string id + data.Credential:1.0.0 credentials +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserDeactivated.vss b/src/main/vlingo/schemata/UserDeactivated.vss new file mode 100644 index 00000000..36ee49e8 --- /dev/null +++ b/src/main/vlingo/schemata/UserDeactivated.vss @@ -0,0 +1,6 @@ +event UserDeactivated { + version semanticVersion + string id + string tenantId + string username +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserProfileReplaced.vss b/src/main/vlingo/schemata/UserProfileReplaced.vss new file mode 100644 index 00000000..1b6a66e5 --- /dev/null +++ b/src/main/vlingo/schemata/UserProfileReplaced.vss @@ -0,0 +1,7 @@ +event UserProfileReplaced { + version semanticVersion + string id + string tenantId + string username + data.Profile:1.0.0 profile +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserRegistered.vss b/src/main/vlingo/schemata/UserRegistered.vss new file mode 100644 index 00000000..7ea36394 --- /dev/null +++ b/src/main/vlingo/schemata/UserRegistered.vss @@ -0,0 +1,8 @@ +event UserRegistered { + version semanticVersion + string id + string tenantId + string username + boolean active + data.Profile:1.0.0 profile +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserUnassignedFromGroup.vss b/src/main/vlingo/schemata/UserUnassignedFromGroup.vss new file mode 100644 index 00000000..cd847504 --- /dev/null +++ b/src/main/vlingo/schemata/UserUnassignedFromGroup.vss @@ -0,0 +1,5 @@ +event UserUnassignedFromGroup { + version semanticVersion + string id + string tenantId +} \ No newline at end of file diff --git a/src/main/vlingo/schemata/UserUnassignedFromRole.vss b/src/main/vlingo/schemata/UserUnassignedFromRole.vss new file mode 100644 index 00000000..b182e349 --- /dev/null +++ b/src/main/vlingo/schemata/UserUnassignedFromRole.vss @@ -0,0 +1,6 @@ +event UserUnassignedFromRole { + version semanticVersion + string id + string tenantId + string name +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/TestProperties.java b/src/test/java/io/vlingo/xoom/auth/infra/resource/TestProperties.java deleted file mode 100644 index ef048c93..00000000 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/TestProperties.java +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright © 2012-2021 VLINGO LABS. All rights reserved. -// -// This Source Code Form is subject to the terms of the -// Mozilla Public License, v. 2.0. If a copy of the MPL -// was not distributed with this file, You can obtain -// one at https://mozilla.org/MPL/2.0/. - -package io.vlingo.xoom.auth.infra.resource; - -import java.util.Properties; - -public class TestProperties { - - public static Properties groupResourceProperties(final Properties properties) { - - properties.setProperty("resource.name.group", "[description, assignGroup, unassignGroup, assignUser, unassignUser, queryGroup, queryInnerGroup, queryPermission, queryRole, queryUser]"); - - properties.setProperty("resource.group.handler", "io.vlingo.xoom.auth.infra.resource.GroupResource"); - properties.setProperty("resource.group.pool", "10"); - properties.setProperty("resource.group.disallowPathParametersWithSlash", "true"); - - properties.setProperty("action.group.description.method", "PATCH"); - properties.setProperty("action.group.description.uri", "/tenants/{tenantId}/groups/{groupName}/description"); - properties.setProperty("action.group.description.to", "changeDescription(String tenantId, String groupName, body:java.lang.String description)"); - properties.setProperty("action.group.description.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.group.assignGroup.method", "PUT"); - properties.setProperty("action.group.assignGroup.uri", "/tenants/{tenantId}/groups/{groupName}/groups"); - properties.setProperty("action.group.assignGroup.to", "assignGroup(String tenantId, String groupName, body:java.lang.String groupName)"); - properties.setProperty("action.group.assignGroup.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.group.unassignGroup.method", "DELETE"); - properties.setProperty("action.group.unassignGroup.uri", "/tenants/{tenantId}/groups/{groupName}/groups/{innerGroupName}"); - properties.setProperty("action.group.unassignGroup.to", "unassignGroup(String tenantId, String groupName, String innerGroupName)"); - properties.setProperty("action.group.unassignGroup.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.group.assignUser.method", "PUT"); - properties.setProperty("action.group.assignUser.uri", "/tenants/{tenantId}/groups/{groupName}/users"); - properties.setProperty("action.group.assignUser.to", "assignUser(String tenantId, String groupName, body:java.lang.String username)"); - properties.setProperty("action.group.assignUser.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.group.unassignUser.method", "DELETE"); - properties.setProperty("action.group.unassignUser.uri", "/tenants/{tenantId}/groups/{groupName}/users/{username}"); - properties.setProperty("action.group.unassignUser.to", "unassignUser(String tenantId, String groupName, String username)"); - properties.setProperty("action.group.unassignUser.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.group.queryGroup.method", "GET"); - properties.setProperty("action.group.queryGroup.uri", "/tenants/{tenantId}/groups/{groupName}"); - properties.setProperty("action.group.queryGroup.to", "queryGroup(String tenantId, String groupName)"); - properties.setProperty("action.group.queryGroup.permission", "io.vlingo.xoom.auth.GroupQuery"); - - properties.setProperty("action.group.queryInnerGroup.method", "GET"); - properties.setProperty("action.group.queryInnerGroup.uri", "/tenants/{tenantId}/groups/{groupName}/groups/{innerGroupName}"); - properties.setProperty("action.group.queryInnerGroup.to", "queryInnerGroup(String tenantId, String groupName, String innerGroupName)"); - properties.setProperty("action.group.queryInnerGroup.permission", "io.vlingo.xoom.auth.GroupQuery"); - - properties.setProperty("action.group.queryPermission.method", "GET"); - properties.setProperty("action.group.queryPermission.uri", "/tenants/{tenantId}/groups/{groupName}/permissions/{permissionName}"); - properties.setProperty("action.group.queryPermission.to", "queryPermission(String tenantId, String groupName, String permissionName)"); - properties.setProperty("action.group.queryPermission.permission", "io.vlingo.xoom.auth.GroupQuery"); - - properties.setProperty("action.group.queryRole.method", "GET"); - properties.setProperty("action.group.queryRole.uri", "/tenants/{tenantId}/groups/{groupName}/roles/{roleName}"); - properties.setProperty("action.group.queryRole.to", "queryRole(String tenantId, String groupName, String roleName)"); - properties.setProperty("action.group.queryRole.permission", "io.vlingo.xoom.auth.GroupQuery"); - - properties.setProperty("action.group.queryUser.method", "GET"); - properties.setProperty("action.group.queryUser.uri", "/tenants/{tenantId}/groups/{groupName}/users/{username}"); - properties.setProperty("action.group.queryUser.to", "queryUser(String tenantId, String groupName, String username)"); - properties.setProperty("action.group.queryUser.permission", "io.vlingo.xoom.auth.GroupQuery"); - - return properties; - } - - public static Properties permissionResourceProperties(final Properties properties) { - properties.setProperty("resource.name.permission", "[enforce, enforceReplacement, forget, description, queryPermission]"); - - properties.setProperty("resource.permission.handler", "io.vlingo.xoom.auth.infra.resource.PermissionResource"); - properties.setProperty("resource.permission.pool", "5"); - properties.setProperty("resource.permission.disallowPathParametersWithSlash", "true"); - - properties.setProperty("action.permission.enforce.method", "PATCH"); - properties.setProperty("action.permission.enforce.uri", "/tenants/{tenantId}/permissions/{permissionName}/constraints"); - properties.setProperty("action.permission.enforce.to", "enforce(String tenantId, String permissionName, body:io.vlingo.xoom.auth.infra.resource.ConstraintData constraintData)"); - properties.setProperty("action.permission.enforce.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.permission.enforceReplacement.method", "PATCH"); - properties.setProperty("action.permission.enforceReplacement.uri", "/tenants/{tenantId}/permissions/{permissionName}/constraints/{constraintName}"); - properties.setProperty("action.permission.enforceReplacement.to", "enforceReplacement(String tenantId, String permissionName, String constraintName, body:io.vlingo.xoom.auth.infra.resource.ConstraintData constraintData)"); - properties.setProperty("action.permission.enforceReplacement.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.permission.forget.method", "DELETE"); - properties.setProperty("action.permission.forget.uri", "/tenants/{tenantId}/permissions/{permissionName}/constraints/{constraintName}"); - properties.setProperty("action.permission.forget.to", "forget(String tenantId, String permissionName, String constraintName)"); - properties.setProperty("action.permission.forget.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.permission.description.method", "PATCH"); - properties.setProperty("action.permission.description.uri", "/tenants/{tenantId}/permissions/{permissionName}/description"); - properties.setProperty("action.permission.description.to", "changeDescription(String tenantId, String permissionName, body:java.lang.String description)"); - properties.setProperty("action.permission.description.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.permission.queryPermission.method", "GET"); - properties.setProperty("action.permission.queryPermission.uri", "/tenants/{tenantId}/permissions/{permissionName}"); - properties.setProperty("action.permission.queryPermission.to", "queryPermission(String tenantId, String permissionName)"); - properties.setProperty("action.permission.queryPermission.permission", "io.vlingo.xoom.auth.Administrator"); - - return properties; - } - - public static Properties roleResourceProperties(final Properties properties) { - properties.setProperty("resource.name.role", "[description, assignGroup, unassignGroup, assignUser, unassignUser, attach, detach, queryRole, queryPermission, queryGroup, queryUser]"); - - properties.setProperty("resource.role.handler", "io.vlingo.xoom.auth.infra.resource.RoleResource"); - properties.setProperty("resource.role.pool", "10"); - properties.setProperty("resource.role.disallowPathParametersWithSlash", "true"); - - properties.setProperty("action.role.description.method", "PATCH"); - properties.setProperty("action.role.description.uri", "/tenants/{tenantId}/roles/{roleName}/description"); - properties.setProperty("action.role.description.to", "changeDescription(String tenantId, String roleName, body:java.lang.String description)"); - properties.setProperty("action.role.description.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.assignGroup.method", "PUT"); - properties.setProperty("action.role.assignGroup.uri", "/tenants/{tenantId}/roles/{roleName}/groups"); - properties.setProperty("action.role.assignGroup.to", "assignGroup(String tenantId, String roleName, body:java.lang.String groupName)"); - properties.setProperty("action.role.assignGroup.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.unassignGroup.method", "DELETE"); - properties.setProperty("action.role.unassignGroup.uri", "/tenants/{tenantId}/roles/{roleName}/groups/{groupName}"); - properties.setProperty("action.role.unassignGroup.to", "unassignGroup(String tenantId, String roleName, String groupName)"); - properties.setProperty("action.role.unassignGroup.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.assignUser.method", "PUT"); - properties.setProperty("action.role.assignUser.uri", "/tenants/{tenantId}/roles/{roleName}/users"); - properties.setProperty("action.role.assignUser.to", "assignUser(String tenantId, String roleName, body:java.lang.String username)"); - properties.setProperty("action.role.assignUser.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.unassignUser.method", "DELETE"); - properties.setProperty("action.role.unassignUser.uri", "/tenants/{tenantId}/roles/{roleName}/users/{username}"); - properties.setProperty("action.role.unassignUser.to", "unassignUser(String tenantId, String roleName, String username)"); - properties.setProperty("action.role.unassignUser.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.attach.method", "PUT"); - properties.setProperty("action.role.attach.uri", "/tenants/{tenantId}/roles/{roleName}/permissions"); - properties.setProperty("action.role.attach.to", "attach(String tenantId, String roleName, body:java.lang.String permissionName)"); - properties.setProperty("action.role.attach.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.detach.method", "DELETE"); - properties.setProperty("action.role.detach.uri", "/tenants/{tenantId}/roles/{roleName}/permissions/{permissionName}"); - properties.setProperty("action.role.detach.to", "detach(String tenantId, String roleName, String permissionName)"); - properties.setProperty("action.role.detach.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.role.queryRole.method", "GET"); - properties.setProperty("action.role.queryRole.uri", "/tenants/{tenantId}/roles/{roleName}"); - properties.setProperty("action.role.queryRole.to", "queryRole(String tenantId, String roleName)"); - properties.setProperty("action.role.queryRole.permission", "io.vlingo.xoom.auth.RoleQuery"); - - properties.setProperty("action.role.queryPermission.method", "GET"); - properties.setProperty("action.role.queryPermission.uri", "/tenants/{tenantId}/roles/{roleName}/permissions/{permissionName}"); - properties.setProperty("action.role.queryPermission.to", "queryPermission(String tenantId, String roleName, String permissionName)"); - properties.setProperty("action.role.queryPermission.permission", "io.vlingo.xoom.auth.RoleQuery"); - - properties.setProperty("action.role.queryGroup.method", "GET"); - properties.setProperty("action.role.queryGroup.uri", "/tenants/{tenantId}/roles/{roleName}/groups/{groupName}"); - properties.setProperty("action.role.queryGroup.to", "queryGroup(String tenantId, String roleName, String groupName)"); - properties.setProperty("action.role.queryGroup.permission", "io.vlingo.xoom.auth.RoleQuery"); - - properties.setProperty("action.role.queryUser.method", "GET"); - properties.setProperty("action.role.queryUser.uri", "/tenants/{tenantId}/roles/{roleName}/users/{username}"); - properties.setProperty("action.role.queryUser.to", "queryUser(String tenantId, String roleName, String username)"); - properties.setProperty("action.role.queryUser.permission", "io.vlingo.xoom.auth.RoleQuery"); - - return properties; - } - - public static Properties tenantResourceProperties() { - final Properties properties = new Properties(); - - properties.setProperty("server.http.port", "8080"); - properties.setProperty("server.dispatcher.pool", "1"); - properties.setProperty("server.buffer.pool.size", "200"); - properties.setProperty("server.message.buffer.size", "10240"); - properties.setProperty("server.probe.interval", "10"); - properties.setProperty("server.probe.timeout", "10"); - properties.setProperty("server.processor.pool.size", "1"); - properties.setProperty("server.request.missing.content.timeout", "100"); - - properties.setProperty("resource.name.tenant", "[subscribe, activate, deactivate, description, name, provisionGroup, provisionPermission, provisionRole, registerUser, queryTenant, queryGroups, queryPermissions, queryRoles, queryUsers]"); - properties.setProperty("resource.tenant.handler", "io.vlingo.xoom.auth.infra.resource.TenantResource"); - properties.setProperty("resource.tenant.pool", "10"); - properties.setProperty("resource.tenant.disallowPathParametersWithSlash", "true"); - - properties.setProperty("action.tenant.subscribe.method", "POST"); - properties.setProperty("action.tenant.subscribe.uri", "/tenants"); - properties.setProperty("action.tenant.subscribe.to", "subscribeFor(body:io.vlingo.xoom.auth.infra.resource.TenantData tenantData)"); - properties.setProperty("action.tenant.subscribe.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.activate.method", "PATCH"); - properties.setProperty("action.tenant.activate.uri", "/tenants/{tenantId}/activate"); - properties.setProperty("action.tenant.activate.to", "activate(String tenantId)"); - properties.setProperty("action.tenant.activate.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.deactivate.method", "PATCH"); - properties.setProperty("action.tenant.deactivate.uri", "/tenants/{tenantId}/deactivate"); - properties.setProperty("action.tenant.deactivate.to", "deactivate(String tenantId)"); - properties.setProperty("action.tenant.deactivate.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.description.method", "PATCH"); - properties.setProperty("action.tenant.description.uri", "/tenants/{tenantId}/description"); - properties.setProperty("action.tenant.description.to", "changeDescription(String tenantId, body:java.lang.String description)"); - properties.setProperty("action.tenant.description.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.name.method", "PATCH"); - properties.setProperty("action.tenant.name.uri", "/tenants/{tenantId}/name"); - properties.setProperty("action.tenant.name.to", "changeName(String tenantId, body:java.lang.String name)"); - properties.setProperty("action.tenant.name.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.provisionGroup.method", "POST"); - properties.setProperty("action.tenant.provisionGroup.uri", "/tenants/{tenantId}/groups"); - properties.setProperty("action.tenant.provisionGroup.to", "provisionGroup(String tenantId, body:io.vlingo.xoom.auth.infra.resource.GroupData groupData)"); - properties.setProperty("action.tenant.provisionGroup.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.provisionPermission.method", "POST"); - properties.setProperty("action.tenant.provisionPermission.uri", "/tenants/{tenantId}/permissions"); - properties.setProperty("action.tenant.provisionPermission.to", "provisionPermission(String tenantId, body:io.vlingo.xoom.auth.infra.resource.PermissionData permissionData)"); - properties.setProperty("action.tenant.provisionPermission.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.provisionRole.method", "POST"); - properties.setProperty("action.tenant.provisionRole.uri", "/tenants/{tenantId}/roles"); - properties.setProperty("action.tenant.provisionRole.to", "provisionRole(String tenantId, body:io.vlingo.xoom.auth.infra.resource.RoleData roleData)"); - properties.setProperty("action.tenant.provisionRole.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.registerUser.method", "POST"); - properties.setProperty("action.tenant.registerUser.uri", "/tenants/{tenantId}/users"); - properties.setProperty("action.tenant.registerUser.to", "registerUser(String tenantId, body:io.vlingo.xoom.auth.infra.resource.UserRegistrationData userData)"); - properties.setProperty("action.tenant.registerUser.permission", "io.vlingo.xoom.auth.TenantRepresentative"); - - properties.setProperty("action.tenant.queryTenant.method", "GET"); - properties.setProperty("action.tenant.queryTenant.uri", "/tenants/{tenantId}"); - properties.setProperty("action.tenant.queryTenant.to", "queryTenant(String tenantId)"); - - properties.setProperty("action.tenant.queryGroups.method", "GET"); - properties.setProperty("action.tenant.queryGroups.uri", "/tenants/{tenantId}/groups"); - properties.setProperty("action.tenant.queryGroups.to", "queryGroups(String tenantId)"); - properties.setProperty("action.tenant.queryGroups.permission", "io.vlingo.xoom.auth.TenantQuery"); - - properties.setProperty("action.tenant.queryPermissions.method", "GET"); - properties.setProperty("action.tenant.queryPermissions.uri", "/tenants/{tenantId}/permissions"); - properties.setProperty("action.tenant.queryPermissions.to", "queryPermissions(String tenantId)"); - properties.setProperty("action.tenant.queryPermissions.permission", "io.vlingo.xoom.auth.TenantQuery"); - - properties.setProperty("action.tenant.queryRoles.method", "GET"); - properties.setProperty("action.tenant.queryRoles.uri", "/tenants/{tenantId}/roles"); - properties.setProperty("action.tenant.queryRoles.to", "queryRoles(String tenantId)"); - properties.setProperty("action.tenant.queryRoles.permission", "io.vlingo.xoom.auth.TenantQuery"); - - properties.setProperty("action.tenant.queryUsers.method", "GET"); - properties.setProperty("action.tenant.queryUsers.uri", "/tenants/{tenantId}/users"); - properties.setProperty("action.tenant.queryUsers.to", "queryUsers(String tenantId)"); - properties.setProperty("action.tenant.queryUsers.permission", "io.vlingo.xoom.auth.TenantQuery"); - - return properties; - } - - public static Properties userResourceProperties(final Properties properties) { - properties.setProperty("resource.name.user", "[activate, deactivate, addCredential, removeCredential, replaceCredential, profile, queryUser, queryPermission, queryRole]"); - - properties.setProperty("resource.user.handler", "io.vlingo.xoom.auth.infra.resource.UserResource"); - properties.setProperty("resource.user.pool", "5"); - properties.setProperty("resource.user.disallowPathParametersWithSlash", "true"); - - properties.setProperty("action.user.activate.method", "PATCH"); - properties.setProperty("action.user.activate.uri", "/tenants/{tenantId}/users/{username}/activate"); - properties.setProperty("action.user.activate.to", "activate(String tenantId, String username)"); - properties.setProperty("action.user.activate.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.user.deactivate.method", "PATCH"); - properties.setProperty("action.user.deactivate.uri", "/tenants/{tenantId}/users/{username}/deactivate"); - properties.setProperty("action.user.deactivate.to", "deactivate(String tenantId, String username)"); - properties.setProperty("action.user.deactivate.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.user.addCredential.method", "PUT"); - properties.setProperty("action.user.addCredential.uri", "/tenants/{tenantId}/users/{username}/credentials"); - properties.setProperty("action.user.addCredential.to", "addCredential(String tenantId, String username, body:io.vlingo.xoom.auth.infra.resource.CredentialData credentialData)"); - properties.setProperty("action.user.addCredential.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.user.removeCredential.method", "DELETE"); - properties.setProperty("action.user.removeCredential.uri", "/tenants/{tenantId}/users/{username}/credentials/{authority}"); - properties.setProperty("action.user.removeCredential.to", "removeCredential(String tenantId, String username, String authority)"); - properties.setProperty("action.user.removeCredential.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.user.replaceCredential.method", "PATCH"); - properties.setProperty("action.user.replaceCredential.uri", "/tenants/{tenantId}/users/{username}/credentials/{authority}"); - properties.setProperty("action.user.replaceCredential.to", "replaceCredential(String tenantId, String username, String authority, body:io.vlingo.xoom.auth.infra.resource.CredentialData credentialData)"); - properties.setProperty("action.user.replaceCredential.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.user.profile.method", "PATCH"); - properties.setProperty("action.user.profile.uri", "/tenants/{tenantId}/users/{username}/profile"); - properties.setProperty("action.user.profile.to", "profile(String tenantId, String username, body:io.vlingo.xoom.auth.infra.resource.ProfileData profileData)"); - properties.setProperty("action.user.profile.permission", "io.vlingo.xoom.auth.Administrator"); - - properties.setProperty("action.user.queryUser.method", "GET"); - properties.setProperty("action.user.queryUser.uri", "/tenants/{tenantId}/users/{username}"); - properties.setProperty("action.user.queryUser.to", "queryUser(String tenantId, String username)"); - properties.setProperty("action.user.queryUser.permission", "io.vlingo.xoom.auth.UserQuery"); - - properties.setProperty("action.user.queryPermission.method", "GET"); - properties.setProperty("action.user.queryPermission.uri", "/tenants/{tenantId}/users/{username}/permissions/{permissionName}"); - properties.setProperty("action.user.queryPermission.to", "queryPermission(String tenantId, String username, String permissionName)"); - properties.setProperty("action.user.queryPermission.permission", "io.vlingo.xoom.auth.UserQuery"); - - properties.setProperty("action.user.queryRole.method", "GET"); - properties.setProperty("action.user.queryRole.uri", "/tenants/{tenantId}/users/{username}/roles/{roleName}"); - properties.setProperty("action.user.queryRole.to", "queryRole(String tenantId, String username, String roleName)"); - properties.setProperty("action.user.queryRole.permission", "io.vlingo.xoom.auth.UserQuery"); - - return properties; - } -} diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/TestResponseChannelConsumer.java b/src/test/java/io/vlingo/xoom/auth/infra/resource/TestResponseChannelConsumer.java deleted file mode 100644 index feb6f480..00000000 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/TestResponseChannelConsumer.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright © 2012-2021 VLINGO LABS. All rights reserved. -// -// This Source Code Form is subject to the terms of the -// Mozilla Public License, v. 2.0. If a copy of the MPL -// was not distributed with this file, You can obtain -// one at https://mozilla.org/MPL/2.0/. - -package io.vlingo.xoom.auth.infra.resource; - -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; - -import io.vlingo.xoom.actors.Actor; -import io.vlingo.xoom.actors.ActorInstantiator; -import io.vlingo.xoom.actors.testkit.TestUntil; -import io.vlingo.xoom.http.Response; -import io.vlingo.xoom.http.ResponseParser; -import io.vlingo.xoom.wire.channel.ResponseChannelConsumer; -import io.vlingo.xoom.wire.message.ConsumerByteBuffer; - -public class TestResponseChannelConsumer extends Actor implements ResponseChannelConsumer { - private ResponseParser parser; - private final Progress progress; - - public TestResponseChannelConsumer(final Progress progress) { - this.progress = progress; - } - - @Override - public void consume(final ConsumerByteBuffer buffer) { - if (parser == null) { - parser = ResponseParser.parserFor(buffer.asByteBuffer()); - } else { - parser.parseNext(buffer.asByteBuffer()); - } - buffer.release(); - while (parser.hasFullResponse()) { - final Response response = parser.fullResponse(); - synchronized (this) { - progress.responses.add(response); - progress.consumeCount.incrementAndGet(); - if (progress.untilConsumed != null) { - progress.untilConsumed.happened(); - } - } - } - } - - public static class Progress { - private TestUntil untilConsumed; - private Queue responses = new ConcurrentLinkedQueue<>(); - private AtomicInteger consumeCount = new AtomicInteger(0); - - public Progress(final int times) { - untilConsumed = TestUntil.happenings(times); - } - - public void completes() { - synchronized (this) { - untilConsumed.completes(); - } - } - - public int consumeCount() { - synchronized (this) { - return consumeCount.get(); - } - } - - public int remaining() { - synchronized (this) { - return untilConsumed.remaining(); - } - } - - public void resetTimes(final int times) { - synchronized (this) { - untilConsumed = TestUntil.happenings(times); - } - } - - public Queue responses() { - synchronized (this) { - return responses; - } - } - } - - public static class TestResponseChannelConsumerInstantiator implements ActorInstantiator { - private static final long serialVersionUID = -8571428261776998164L; - - private final Progress progress; - - public TestResponseChannelConsumerInstantiator(final Progress progress) { - this.progress = progress; - } - - @Override - public TestResponseChannelConsumer instantiate() { - return new TestResponseChannelConsumer(progress); - } - - @Override - public Class type() { - return TestResponseChannelConsumer.class; - } - } -} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/CountingDispatcherControl.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/CountingDispatcherControl.java new file mode 100644 index 00000000..d07fed58 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/CountingDispatcherControl.java @@ -0,0 +1,39 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.lattice.model.projection.ProjectionControl; +import io.vlingo.xoom.symbio.store.Result; +import io.vlingo.xoom.symbio.store.dispatch.ConfirmDispatchedResultInterest; +import io.vlingo.xoom.symbio.store.dispatch.DispatcherControl; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class CountingDispatcherControl implements DispatcherControl { + private AccessSafely access; + private final Map confirmations = new ConcurrentHashMap<>(); + + public AccessSafely afterCompleting(final int times) { + access = AccessSafely.afterCompleting(times); + access.writingWith("confirmations", (String projectionId) -> { + final int count = confirmations.getOrDefault(projectionId, 0); + confirmations.put(projectionId, count + 1); + }); + access.readingWith("confirmations", () -> confirmations); + return access; + } + + @Override + public void confirmDispatched(final String dispatchId, final ConfirmDispatchedResultInterest interest) { + access.writeUsing("confirmations", dispatchId); + interest.confirmDispatchedResultedIn(Result.Success, dispatchId); + } + + @Override + public void dispatchUnconfirmed() { + } + + @Override + public void stop() { + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionTest.java new file mode 100644 index 00000000..05409ff7 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionTest.java @@ -0,0 +1,206 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.group.*; +import io.vlingo.xoom.auth.model.role.GroupAssignedToRole; +import io.vlingo.xoom.auth.model.role.GroupUnassignedFromRole; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.RoleProvisioned; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static org.junit.jupiter.api.Assertions.*; + +public class GroupProjectionTest extends ProjectionTest { + + @Test + public void itProjectsProvisionedGroup() { + final GroupId groupId = GroupId.from(TenantId.unique(), "group-a"); + + givenEvents( + new GroupProvisioned(groupId, "group-a", "Group A description") + ); + + assertCompletes(groupOf(groupId), group -> { + assertEquals(groupId.idString(), group.id); + assertEquals(groupId.tenantId.idString(), group.tenantId); + assertEquals("group-a", group.name); + assertEquals("Group A description", group.description); + }); + } + + @Test + public void itProjectsDescriptionChange() { + final GroupId groupId = GroupId.from(TenantId.unique(), "group-a"); + + givenEvents( + new GroupProvisioned(groupId, "group-a", "Group A description"), + new GroupDescriptionChanged(groupId, "Group A improved") + ); + + assertCompletes(groupOf(groupId), group -> { + assertEquals("group-a", group.name); + assertEquals("Group A improved", group.description); + }); + } + + @Test + public void itProjectsGroupAssignedToAnotherGroup() { + final GroupId groupIdA = GroupId.from(TenantId.unique(), "group-a"); + final GroupId groupIdB = GroupId.from(TenantId.unique(), "group-b"); + + givenEvents( + new GroupProvisioned(groupIdA, "group-a", "Group A"), + new GroupProvisioned(groupIdB, "group-b", "Group B"), + new GroupAssignedToGroup(groupIdA, groupIdB) + ); + + assertCompletes(groupOf(groupIdA), group -> { + assertEquals(setOf(Relation.groupWithMember(groupIdA, groupIdB)), group.groups); + assertFalse(group.hasMember(groupIdA)); + assertTrue(group.hasMember(groupIdB)); + }); + } + + @Test + public void itProjectsGroupUnassignedFromAnotherGroup() { + final GroupId groupIdA = GroupId.from(TenantId.unique(), "group-a"); + final GroupId groupIdB = GroupId.from(TenantId.unique(), "group-b"); + final GroupId groupIdC = GroupId.from(TenantId.unique(), "group-c"); + + givenEvents( + new GroupProvisioned(groupIdA, "group-a", "Group A"), + new GroupProvisioned(groupIdB, "group-b", "Group B"), + new GroupProvisioned(groupIdC, "group-c", "Group C"), + new GroupAssignedToGroup(groupIdA, groupIdB), + new GroupAssignedToGroup(groupIdA, groupIdC), + new GroupUnassignedFromGroup(groupIdA, groupIdB) + ); + + assertCompletes(groupOf(groupIdA), group -> { + assertEquals(setOf(Relation.groupWithMember(groupIdA, groupIdC)), group.groups); + assertFalse(group.hasMember(groupIdA)); + assertFalse(group.hasMember(groupIdB)); + assertTrue(group.hasMember(groupIdC)); + }); + } + + @Test + public void itProjectsUserAssignedToGroup() { + final TenantId tenantId = TenantId.unique(); + final GroupId groupId = GroupId.from(tenantId, "group-a"); + final UserId userId = UserId.from(tenantId, "bobby"); + + givenEvents( + new GroupProvisioned(groupId, "group-a", "Group A"), + new UserAssignedToGroup(groupId, userId) + ); + + assertCompletes(groupOf(groupId), group -> { + assertEquals(setOf(Relation.userAssignedToGroup(userId, groupId)), group.users); + assertTrue(group.hasMember(userId)); + }); + } + + @Test + public void itProjectsUserUnassignedFromGroup() { + final TenantId tenantId = TenantId.unique(); + final GroupId groupId = GroupId.from(tenantId, "group-a"); + final UserId userIdBobby = UserId.from(tenantId, "bobby"); + final UserId userIdAlice = UserId.from(tenantId, "alice"); + + givenEvents( + new GroupProvisioned(groupId, "group-a", "Group A"), + new UserAssignedToGroup(groupId, userIdBobby), + new UserAssignedToGroup(groupId, userIdAlice), + new UserUnassignedFromGroup(groupId, userIdBobby) + ); + + assertCompletes(groupOf(groupId), group -> { + assertEquals(setOf(Relation.userAssignedToGroup(userIdAlice, groupId)), group.users); + assertTrue(group.hasMember(userIdAlice)); + assertFalse(group.hasMember(userIdBobby)); + }); + } + + @Test + public void itProjectsGroupEventsToGroupView() { + final TenantId tenantId = TenantId.unique(); + final GroupId groupIdA = GroupId.from(tenantId, "group-a"); + final GroupId groupIdB = GroupId.from(TenantId.unique(), "group-b"); + final UserId userId = UserId.from(tenantId, "bobby"); + + givenEvents( + new GroupProvisioned(groupIdA, "group-a", "Group A"), + new GroupProvisioned(groupIdB, "group-b", "Group B"), + new GroupAssignedToGroup(groupIdA, groupIdB), + new UserAssignedToGroup(groupIdA, userId), + new GroupDescriptionChanged(groupIdA, "Group A improved") + ); + + assertCompletes(groupOf(groupIdA), group -> { + assertEquals(setOf(Relation.groupWithMember(groupIdA, groupIdB)), group.groups); + assertEquals(setOf(Relation.userAssignedToGroup(userId, groupIdA)), group.users); + assertEquals("Group A improved", group.description); + }); + } + + @Test + public void itProjectsGroupAssignedToRole() { + final TenantId tenantId = TenantId.unique(); + final GroupId groupId = GroupId.from(tenantId, "group-a"); + final RoleId roleIdA = RoleId.from(tenantId, "role-a"); + final RoleId roleIdB = RoleId.from(tenantId, "role-b"); + + givenEvents( + new GroupProvisioned(groupId, "group-a", "Group A"), + new RoleProvisioned(roleIdA, "role-a", "Role A"), + new RoleProvisioned(roleIdB, "role-b", "Role B"), + new GroupAssignedToRole(roleIdA, groupId), + new GroupAssignedToRole(roleIdB, groupId) + ); + + assertCompletes(groupOf(groupId), group -> { + assertEquals(setOf(Relation.groupAssignedToRole(groupId, roleIdA), Relation.groupAssignedToRole(groupId, roleIdB)), group.roles); + assertTrue(group.isInRole(roleIdA)); + assertTrue(group.isInRole(roleIdB)); + }); + } + + @Test + public void itProjectsGroupUnassignedFromRole() { + final TenantId tenantId = TenantId.unique(); + final GroupId groupId = GroupId.from(tenantId, "group-a"); + final RoleId roleIdA = RoleId.from(tenantId, "role-a"); + final RoleId roleIdB = RoleId.from(tenantId, "role-b"); + + givenEvents( + new GroupProvisioned(groupId, "group-a", "Group A"), + new RoleProvisioned(roleIdA, "role-a", "Role A"), + new RoleProvisioned(roleIdB, "role-b", "Role B"), + new GroupAssignedToRole(roleIdA, groupId), + new GroupAssignedToRole(roleIdB, groupId), + new GroupUnassignedFromRole(roleIdA, groupId) + ); + + assertCompletes(groupOf(groupId), group -> { + assertEquals(setOf(Relation.groupAssignedToRole(groupId, roleIdB)), group.roles); + assertFalse(group.isInRole(roleIdA)); + assertTrue(group.isInRole(roleIdB)); + }); + } + + private Completes groupOf(final GroupId groupId) { + return world.actorFor(GroupQueries.class, GroupQueriesActor.class, stateStore).groupOf(groupId); + } + + private Set setOf(T... elements) { + return new HashSet(Arrays.asList(elements)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesTest.java new file mode 100644 index 00000000..b32ea6af --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesTest.java @@ -0,0 +1,93 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.common.Outcome; +import io.vlingo.xoom.lattice.model.stateful.StatefulTypeRegistry; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.Result; +import io.vlingo.xoom.symbio.store.StorageException; +import io.vlingo.xoom.symbio.store.dispatch.NoOpDispatcher; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.symbio.store.state.inmemory.InMemoryStateStoreActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertContains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GroupQueriesTest { + + private World world; + private StateStore stateStore; + private GroupQueries queries; + + @BeforeEach + public void setUp() { + world = World.startWithDefaults("test-state-store-query"); + stateStore = world.actorFor(StateStore.class, InMemoryStateStoreActor.class, Collections.singletonList(new NoOpDispatcher())); + StatefulTypeRegistry.registerAll(world, stateStore, GroupView.class); + queries = world.actorFor(GroupQueries.class, GroupQueriesActor.class, stateStore); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void itQueriesTheGroupById() { + final GroupId firstGroupId = GroupId.from(TenantId.from("e79e02c5-735f-4998-b414-938479650be0"), "first-group-name"); + final GroupView firstGroup = GroupView.from(firstGroupId, "first-group-name", "first-group-description"); + final GroupId secondGroupId = GroupId.from(TenantId.from("96bf1fd1-9bdc-4352-99b4-8089e28cfaa3"), "second-group-name"); + final GroupView secondGroup = GroupView.from(secondGroupId, "second-group-name", "second-group-description"); + + givenGroupsExist(firstGroup, secondGroup); + + assertCompletes(queries.groupOf(firstGroupId), group -> assertEquals(firstGroup, group)); + assertCompletes(queries.groupOf(secondGroupId), group -> assertEquals(secondGroup, group)); + } + + @Test + public void itQueriesAllGroups() { + final GroupId firstGroupId = GroupId.from(TenantId.from("e79e02c5-735f-4998-b414-938479650be0"), "first-group-name"); + final GroupView firstGroup = GroupView.from(firstGroupId, "first-group-name", "first-group-description"); + final GroupId secondGroupId = GroupId.from(TenantId.from("96bf1fd1-9bdc-4352-99b4-8089e28cfaa3"), "second-group-name"); + final GroupView secondGroup = GroupView.from(secondGroupId, "second-group-name", "second-group-description"); + + givenGroupsExist(firstGroup, secondGroup); + + final Completes> outcome = queries.groups(); + + assertCompletes(outcome, groups -> { + assertContains(firstGroup, groups); + assertContains(secondGroup, groups); + }); + } + + @Test + public void itReturnsAnEmptyGroupIfItIsNotFound() { + final Completes result = queries.groupOf(GroupId.from(TenantId.from("5e39f013-27f4-434d-8f8a-dba940baed7c"), "G2")); + + assertCompletes(result, group -> assertEquals("", group.id)); + } + + private static final StateStore.WriteResultInterest NOOP_WRITER = new StateStore.WriteResultInterest() { + @Override + public void writeResultedIn(Outcome outcome, String s, S s1, int i, List> list, Object o) { + } + }; + + private void givenGroupsExist(final GroupView... groups) { + Arrays.stream(groups).forEach(group -> stateStore.write(group.id, group, 1, NOOP_WRITER)); + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/MockDispatcher.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/MockDispatcher.java new file mode 100644 index 00000000..f43c6bb1 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/MockDispatcher.java @@ -0,0 +1,43 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.symbio.Entry; +import io.vlingo.xoom.symbio.State; +import io.vlingo.xoom.symbio.store.dispatch.Dispatchable; +import io.vlingo.xoom.symbio.store.dispatch.Dispatcher; +import io.vlingo.xoom.symbio.store.dispatch.DispatcherControl; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class MockDispatcher implements Dispatcher, State>> { + + private AccessSafely access; + + private List> entries = new CopyOnWriteArrayList<>(); + + public MockDispatcher() { + super(); + this.access = afterCompleting(0); + } + + public AccessSafely afterCompleting(final int times) { + access = AccessSafely + .afterCompleting(times) + .writingWith("appendedAll", (List> appended) -> entries.addAll(appended)) + .readingWith("appendedAt", (Integer index) -> entries.get(index)) + .readingWith("entriesCount", () -> entries.size()); + + return access; + } + + @Override + public void controlWith(final DispatcherControl control) { + + } + + @Override + public void dispatch(final Dispatchable, State> dispatchable) { + access.writeUsing("appendedAll", dispatchable.entries()); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionTest.java new file mode 100644 index 00000000..6874025b --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionTest.java @@ -0,0 +1,153 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.persistence.PermissionView.ConstraintView; +import io.vlingo.xoom.auth.model.permission.*; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.RolePermissionAttached; +import io.vlingo.xoom.auth.model.role.RolePermissionDetached; +import io.vlingo.xoom.auth.model.role.RoleProvisioned; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.value.Constraint; +import io.vlingo.xoom.common.Completes; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static io.vlingo.xoom.auth.test.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +public class PermissionProjectionTest extends ProjectionTest { + + @Test + public void itProjectsProvisionedPermission() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A") + ); + + assertCompletes(permissionOf(permissionId), permission -> { + assertEquals(permissionId.idString(), permission.id); + assertEquals(permissionId.tenantId.idString(), permission.tenantId); + assertEquals("permission-a", permission.name); + assertEquals("Permission A", permission.description); + }); + } + + @Test + public void itProjectsEnforcedConstraint() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + final Constraint constraintA = Constraint.from(Constraint.Type.String, "constraint-a", "2", "Constraint A"); + final Constraint constraintB = Constraint.from(Constraint.Type.String, "constraint-b", "B", "Constraint B"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A"), + new PermissionConstraintEnforced(permissionId, constraintA), + new PermissionConstraintEnforced(permissionId, constraintB) + ); + + assertCompletes(permissionOf(permissionId), permission -> { + assertContains(ConstraintView.from(constraintA), permission.constraints); + assertContains(ConstraintView.from(constraintB), permission.constraints); + }); + } + + @Test + public void itProjectsEnforcedConstraintReplacement() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + final Constraint constraintA = Constraint.from(Constraint.Type.String, "constraint-a", "2", "Constraint A"); + final Constraint constraintB = Constraint.from(Constraint.Type.String, "constraint-b", "B", "Constraint B"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A"), + new PermissionConstraintEnforced(permissionId, constraintA), + new PermissionConstraintReplacementEnforced(permissionId, "constraint-a", constraintB) + ); + + assertCompletes(permissionOf(permissionId), permission -> { + assertNotContains(ConstraintView.from(constraintA), permission.constraints); + assertContains(ConstraintView.from(constraintB), permission.constraints); + }); + } + + @Test + public void itProjectsForgottenConstraint() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + final Constraint constraintA = Constraint.from(Constraint.Type.String, "constraint-a", "2", "Constraint A"); + final Constraint constraintB = Constraint.from(Constraint.Type.String, "constraint-b", "B", "Constraint B"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A"), + new PermissionConstraintEnforced(permissionId, constraintA), + new PermissionConstraintEnforced(permissionId, constraintB), + new PermissionConstraintForgotten(permissionId, "constraint-a") + ); + + assertCompletes(permissionOf(permissionId), permission -> { + assertNotContains(ConstraintView.from(constraintA), permission.constraints); + assertContains(ConstraintView.from(constraintB), permission.constraints); + }); + } + + @Test + public void itProjectsConstraintDescriptionChange() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A"), + new PermissionDescriptionChanged(permissionId, "Permission A updated") + ); + + assertCompletes(permissionOf(permissionId), permission -> assertEquals("Permission A updated", permission.description)); + } + + @Test + public void itProjectsRolePermissionAttached() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + final RoleId roleId = RoleId.from(permissionId.tenantId, "role-a"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A"), + new RoleProvisioned(roleId, "role-a", "Role A"), + new RolePermissionAttached(roleId, permissionId) + ); + + assertCompletes(permissionOf(permissionId), permission -> { + assertEquals(setOf(Relation.roleWithPermission(roleId, permissionId)), permission.roles); + assertTrue(permission.isAttachedTo(roleId)); + assertFalse(permission.isAttachedTo(RoleId.from(permissionId.tenantId, "role-b"))); + }); + } + + @Test + public void itProjectsRolePermissionDetached() { + final PermissionId permissionId = PermissionId.from(TenantId.unique(), "permission-a"); + final RoleId roleIdA = RoleId.from(permissionId.tenantId, "role-a"); + final RoleId roleIdB = RoleId.from(permissionId.tenantId, "role-b"); + + givenEvents( + new PermissionProvisioned(permissionId, "permission-a", "Permission A"), + new RoleProvisioned(roleIdA, "role-a", "Role A"), + new RoleProvisioned(roleIdB, "role-b", "Role B"), + new RolePermissionAttached(roleIdA, permissionId), + new RolePermissionAttached(roleIdB, permissionId), + new RolePermissionDetached(roleIdA, permissionId) + ); + + assertCompletes(permissionOf(permissionId), permission -> { + assertEquals(setOf(Relation.roleWithPermission(roleIdB, permissionId)), permission.roles); + assertFalse(permission.isAttachedTo(roleIdA)); + assertTrue(permission.isAttachedTo(roleIdB)); + }); + } + + private Completes permissionOf(PermissionId permissionId) { + return world.actorFor(PermissionQueries.class, PermissionQueriesActor.class, stateStore).permissionOf(permissionId); + } + + private Set setOf(T... elements) { + return new HashSet<>(Arrays.asList(elements)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesTest.java new file mode 100644 index 00000000..077caf3a --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesTest.java @@ -0,0 +1,91 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.common.Outcome; +import io.vlingo.xoom.lattice.model.stateful.StatefulTypeRegistry; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.Result; +import io.vlingo.xoom.symbio.store.StorageException; +import io.vlingo.xoom.symbio.store.dispatch.NoOpDispatcher; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.symbio.store.state.inmemory.InMemoryStateStoreActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertContains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PermissionQueriesTest { + + private World world; + private StateStore stateStore; + private PermissionQueries queries; + + @BeforeEach + public void setUp() { + world = World.startWithDefaults("test-state-store-query"); + stateStore = world.actorFor(StateStore.class, InMemoryStateStoreActor.class, Collections.singletonList(new NoOpDispatcher())); + StatefulTypeRegistry.registerAll(world, stateStore, PermissionView.class); + queries = world.actorFor(PermissionQueries.class, PermissionQueriesActor.class, stateStore); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void itQueriesThePermissionById() { + final PermissionId firstPermissionId = PermissionId.from(TenantId.from("8844bb24-811a-45c7-b98b-ba7a88a42372"), "first-permission-name"); + final PermissionView firstPermission = PermissionView.from(firstPermissionId, new HashSet<>(), "first-permission-name", "first-permission-description", new HashSet<>()); + final PermissionId secondPermissionId = PermissionId.from(TenantId.from("2f50fc24-85b1-4657-b876-82491bfc3a70"), "second-permission-name"); + final PermissionView secondPermission = PermissionView.from(secondPermissionId, new HashSet<>(), "second-permission-name", "second-permission-description", new HashSet<>()); + + givenPermissionsExist(firstPermission, secondPermission); + + assertCompletes(queries.permissionOf(firstPermissionId), permission -> assertEquals(firstPermission, permission)); + assertCompletes(queries.permissionOf(secondPermissionId), permission -> assertEquals(secondPermission, permission)); + } + + @Test + public void itQueriesAllPermissions() { + final PermissionId firstPermissionId = PermissionId.from(TenantId.from("8844bb24-811a-45c7-b98b-ba7a88a42372"), "first-permission-name"); + final PermissionView firstPermission = PermissionView.from(firstPermissionId, new HashSet<>(), "first-permission-name", "first-permission-description", new HashSet<>()); + final PermissionId secondPermissionId = PermissionId.from(TenantId.from("2f50fc24-85b1-4657-b876-82491bfc3a70"), "second-permission-name"); + final PermissionView secondPermission = PermissionView.from(secondPermissionId, new HashSet<>(), "second-permission-name", "second-permission-description", new HashSet<>()); + + givenPermissionsExist(firstPermission, secondPermission); + + final Completes> outcome = queries.permissions(); + + assertCompletes(outcome, permissions -> { + assertContains(firstPermission, permissions); + assertContains(secondPermission, permissions); + }); + } + + @Test + public void itReturnsAnEmptyPermissionIfItIsNotFound() { + final Completes result = queries.permissionOf(PermissionId.from(TenantId.from("02e46626-a06c-483d-a4dd-dd829c918a83"), "P1")); + + assertCompletes(result, permission -> assertEquals("", permission.id)); + } + + private static final StateStore.WriteResultInterest NOOP_WRITER = new StateStore.WriteResultInterest() { + @Override + public void writeResultedIn(Outcome outcome, String s, S s1, int i, List> list, Object o) { + + } + }; + + private void givenPermissionsExist(final PermissionView... permissions) { + Arrays.stream(permissions).forEach(permission -> stateStore.write(permission.id, permission, 1, NOOP_WRITER)); + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/ProjectionTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/ProjectionTest.java new file mode 100644 index 00000000..0b5b9932 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/ProjectionTest.java @@ -0,0 +1,71 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.Configuration; +import io.vlingo.xoom.actors.UUIDAddressFactory; +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.actors.testkit.TestWorld; +import io.vlingo.xoom.common.identity.IdentityGeneratorType; +import io.vlingo.xoom.common.serialization.JsonSerialization; +import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent; +import io.vlingo.xoom.lattice.model.projection.Projection; +import io.vlingo.xoom.lattice.model.stateful.StatefulTypeRegistry; +import io.vlingo.xoom.symbio.BaseEntry; +import io.vlingo.xoom.symbio.Metadata; +import io.vlingo.xoom.symbio.store.dispatch.Dispatchable; +import io.vlingo.xoom.symbio.store.dispatch.Dispatcher; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.turbo.ComponentRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public abstract class ProjectionTest { + protected World world; + protected TestWorld testWorld; + protected StateStore stateStore; + protected Projection projection; + + @BeforeEach + public void setUp() throws Exception { + testWorld = TestWorld.start("test-projection", Configuration.define().with(new UUIDAddressFactory(IdentityGeneratorType.RANDOM))); + world = testWorld.world(); + stateStore = QueryModelStateStoreProvider.using(world.stage(), new StatefulTypeRegistry(world)).store; + ProjectionDispatcherProvider.using(world.stage()); + } + + @AfterEach + public void tearDown() { + testWorld.terminate(); + ComponentRegistry.clear(); + } + + protected void givenEvents(IdentifiedDomainEvent... events) { + final Dispatcher storeDispatcher = ComponentRegistry.withType(ProjectionDispatcherProvider.class).storeDispatcher; + final CountingDispatcherControl control = new CountingDispatcherControl(); + final AccessSafely access = control.afterCompleting(events.length); + final Map valueToProjectionId = new HashMap<>(); + final Map entryVersions = new HashMap<>(); + + storeDispatcher.controlWith(control); + + Arrays.stream(events).forEach(event -> { + Integer entryVersion = entryVersions.getOrDefault(event.identity(), 0) + 1; + entryVersions.put(event.identity(), entryVersion); + BaseEntry.TextEntry textEntry = new BaseEntry.TextEntry(event.getClass(), 1, JsonSerialization.serialized(event), entryVersion, Metadata.withObject(event)); + + final String projectionId = UUID.randomUUID().toString(); + valueToProjectionId.put(event.identity(), projectionId); + + storeDispatcher.dispatch(new Dispatchable(projectionId, LocalDateTime.now(), null, Collections.singletonList(textEntry))); + }); + + final Map confirmations = access.readFrom("confirmations"); + assertEquals(events.length, confirmations.size()); + Arrays.stream(events).forEach(event -> assertEquals(1, confirmations.get(valueToProjectionId.get(event.identity())))); + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RelationTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RelationTest.java new file mode 100644 index 00000000..5c28e003 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RelationTest.java @@ -0,0 +1,76 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RelationTest { + + public static final TenantId TENANT_ID = TenantId.unique(); + + @Test + public void itInvertsTheGroupToGroupRelation() { + final GroupId parentGroupId = GroupId.from(TENANT_ID, "group-a"); + final GroupId childGroupId = GroupId.from(TENANT_ID, "group-b"); + + final Relation relation = Relation.groupWithMember(parentGroupId, childGroupId); + final Relation invertedRelation = Relation.groupWithParent(childGroupId, parentGroupId); + + assertEquals(invertedRelation, relation.invert()); + assertEquals(relation, invertedRelation.invert()); + } + + @Test + public void itInvertsTheRoleToPermissionRelation() { + final RoleId roleId = RoleId.from(TENANT_ID, "role-a"); + final PermissionId permissionId = PermissionId.from(TENANT_ID, "permission-a"); + + final Relation relation = Relation.roleWithPermission(roleId, permissionId); + final Relation invertedRelation = Relation.permissionAttachedToRole(permissionId, roleId); + + assertEquals(invertedRelation, relation.invert()); + assertEquals(relation, invertedRelation.invert()); + } + + @Test + public void itInvertsTheRoleToGroupRelation() { + final RoleId roleId = RoleId.from(TENANT_ID, "role-a"); + final GroupId groupId = GroupId.from(TENANT_ID, "group-a"); + + final Relation relation = Relation.roleWithGroup(roleId, groupId); + final Relation invertedRelation = Relation.groupAssignedToRole(groupId, roleId); + + assertEquals(invertedRelation, relation.invert()); + assertEquals(relation, invertedRelation.invert()); + } + + + @Test + public void itInvertsTheRoleToUserRelation() { + final RoleId roleId = RoleId.from(TENANT_ID, "role-a"); + final UserId userId = UserId.from(roleId.tenantId, "alice"); + + final Relation relation = Relation.userAssignedToRole(userId, roleId); + final Relation invertedRelation = Relation.roleWithUser(roleId, userId); + + assertEquals(invertedRelation, relation.invert()); + assertEquals(relation, invertedRelation.invert()); + } + + @Test + public void itInvertsTheUserToGroupRelation() { + final UserId userId = UserId.from(TENANT_ID, "alice"); + final GroupId groupId = GroupId.from(userId.tenantId, "group-a"); + + final Relation relation = Relation.userAssignedToGroup(userId, groupId); + final Relation invertedRelation = Relation.groupWithMember(groupId, userId); + + assertEquals(invertedRelation, relation.invert()); + assertEquals(relation, invertedRelation.invert()); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionTest.java new file mode 100644 index 00000000..e126badd --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionTest.java @@ -0,0 +1,189 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.group.GroupProvisioned; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.permission.PermissionProvisioned; +import io.vlingo.xoom.auth.model.role.*; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.user.UserRegistered; +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.PersonName; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.Completes; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static org.junit.jupiter.api.Assertions.*; + +public class RoleProjectionTest extends ProjectionTest { + + @Test + public void itProjectsTheProvisionedRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A") + ); + + assertCompletes(roleOf(roleId), role -> { + assertEquals(roleId.idString(), role.id); + assertEquals(roleId.tenantId.idString(), role.tenantId); + assertEquals("role-a", role.name); + assertEquals("Role A", role.description); + }); + } + + @Test + public void itProjectsTheRoleDescriptionUpdate() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new RoleDescriptionChanged(roleId, "Role A updated") + ); + + assertCompletes(roleOf(roleId), role -> assertEquals("Role A updated", role.description)); + } + + @Test + public void itProjectsPermissionAttachedToRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + final PermissionId permissionIdA = PermissionId.from(roleId.tenantId, "permission-a"); + final PermissionId permissionIdB = PermissionId.from(roleId.tenantId, "permission-b"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new PermissionProvisioned(permissionIdA, "permission-a", "Permission A"), + new PermissionProvisioned(permissionIdB, "permission-b", "Permission B"), + new RolePermissionAttached(roleId, permissionIdA), + new RolePermissionAttached(roleId, permissionIdB) + ); + + assertCompletes(roleOf(roleId), role -> assertEquals( + setOf(Relation.permissionAttachedToRole(permissionIdA, roleId), Relation.permissionAttachedToRole(permissionIdB, roleId)), + role.permissions + )); + } + + @Test + public void itProjectsPermissionDetachedFromRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + final PermissionId permissionIdA = PermissionId.from(roleId.tenantId, "permission-a"); + final PermissionId permissionIdB = PermissionId.from(roleId.tenantId, "permission-b"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new PermissionProvisioned(permissionIdA, "permission-a", "Permission A"), + new PermissionProvisioned(permissionIdB, "permission-b", "Permission B"), + new RolePermissionAttached(roleId, permissionIdA), + new RolePermissionAttached(roleId, permissionIdB), + new RolePermissionDetached(roleId, permissionIdA) + ); + + assertCompletes(roleOf(roleId), role -> assertEquals( + setOf(Relation.permissionAttachedToRole(permissionIdB, roleId)), + role.permissions + )); + } + + @Test + public void itProjectsGroupAssignedToRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + final GroupId groupIdA = GroupId.from(roleId.tenantId, "group-a"); + final GroupId groupIdB = GroupId.from(roleId.tenantId, "group-b"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new GroupProvisioned(groupIdA, "group-a", "Group A"), + new GroupProvisioned(groupIdB, "group-b", "Group B"), + new GroupAssignedToRole(roleId, groupIdA), + new GroupAssignedToRole(roleId, groupIdB) + ); + + assertCompletes(roleOf(roleId), role -> assertEquals( + setOf(Relation.groupAssignedToRole(groupIdA, roleId), Relation.groupAssignedToRole(groupIdB, roleId)), + role.groups + )); + } + + + @Test + public void itProjectsGroupUnassignedFromRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + final GroupId groupIdA = GroupId.from(roleId.tenantId, "group-a"); + final GroupId groupIdB = GroupId.from(roleId.tenantId, "group-b"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new GroupProvisioned(groupIdA, "group-a", "Group A"), + new GroupProvisioned(groupIdB, "group-b", "Group B"), + new GroupAssignedToRole(roleId, groupIdA), + new GroupAssignedToRole(roleId, groupIdB), + new GroupUnassignedFromRole(roleId, groupIdA) + ); + + assertCompletes(roleOf(roleId), role -> assertEquals( + setOf(Relation.groupAssignedToRole(groupIdB, roleId)), + role.groups + )); + } + + @Test + public void itProjectsUserAssignedToRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + final UserId userIdAlice = UserId.from(roleId.tenantId, "alice"); + final UserId userIdBob = UserId.from(roleId.tenantId, "bob"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new UserRegistered(userIdAlice, "alice", Profile.from("alice@example.com", PersonName.from("Alice", "Hannah", "Little"), "07777123123"), Collections.emptySet(), true), + new UserRegistered(userIdBob, "bobby", Profile.from("bobby@example.com", PersonName.from("Bobby", "Tables", "Little"), "07777123123"), Collections.emptySet(), true), + new UserAssignedToRole(roleId, userIdAlice), + new UserAssignedToRole(roleId, userIdBob) + ); + + assertCompletes(roleOf(roleId), role -> { + assertEquals(setOf(Relation.userAssignedToRole(userIdAlice, roleId), Relation.userAssignedToRole(userIdBob, roleId)), role.users); + assertTrue(role.isInRole(userIdAlice)); + assertTrue(role.isInRole(userIdBob)); + }); + } + + + @Test + public void itProjectsUserUnassignedFromRole() { + final RoleId roleId = RoleId.from(TenantId.unique(), "role-a"); + final UserId userIdAlice = UserId.from(roleId.tenantId, "alice"); + final UserId userIdBob = UserId.from(roleId.tenantId, "bob"); + + givenEvents( + new RoleProvisioned(roleId, "role-a", "Role A"), + new UserRegistered(userIdAlice, "alice", Profile.from("alice@example.com", PersonName.from("Alice", "Hannah", "Little"), "07777123123"), Collections.emptySet(), true), + new UserRegistered(userIdBob, "bobby", Profile.from("bobby@example.com", PersonName.from("Bobby", "Tables", "Little"), "07777123123"), Collections.emptySet(), true), + new UserAssignedToRole(roleId, userIdAlice), + new UserAssignedToRole(roleId, userIdBob), + new UserUnassignedFromRole(roleId, userIdAlice) + ); + + assertCompletes(roleOf(roleId), role -> { + assertEquals(setOf(Relation.userAssignedToRole(userIdBob, roleId)), role.users); + assertFalse(role.isInRole(userIdAlice)); + assertTrue(role.isInRole(userIdBob)); + }); + } + + private Completes roleOf(final RoleId roleId) { + return world.actorFor(RoleQueries.class, RoleQueriesActor.class, stateStore).roleOf(roleId); + } + + private Set setOf(T... elements) { + return new HashSet<>(Arrays.asList(elements)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesTest.java new file mode 100644 index 00000000..bc489d5d --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesTest.java @@ -0,0 +1,93 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.common.Outcome; +import io.vlingo.xoom.lattice.model.stateful.StatefulTypeRegistry; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.Result; +import io.vlingo.xoom.symbio.store.StorageException; +import io.vlingo.xoom.symbio.store.dispatch.NoOpDispatcher; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.symbio.store.state.inmemory.InMemoryStateStoreActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertContains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RoleQueriesTest { + + private World world; + private StateStore stateStore; + private RoleQueries queries; + + @BeforeEach + public void setUp() { + world = World.startWithDefaults("test-state-store-query"); + stateStore = world.actorFor(StateStore.class, InMemoryStateStoreActor.class, Collections.singletonList(new NoOpDispatcher())); + StatefulTypeRegistry.registerAll(world, stateStore, RoleView.class); + queries = world.actorFor(RoleQueries.class, RoleQueriesActor.class, stateStore); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void itQueriesTheRoleById() { + final RoleId firstRoleId = RoleId.from(TenantId.from("06f22ad4-49f5-46a4-b350-f6198a7646a3"), "first-role-name"); + final RoleView firstRole = RoleView.from(firstRoleId, "first-role-name", "first-role-description"); + final RoleId secondRoleId = RoleId.from(TenantId.from("3f51d0fd-335e-41b0-b57c-766470cf6ad7"), "second-role-name"); + final RoleView secondRole = RoleView.from(secondRoleId, "second-role-name", "second-role-description"); + + givenRolesExist(firstRole, secondRole); + + assertCompletes(queries.roleOf(firstRoleId), role -> assertEquals(firstRole, role)); + assertCompletes(queries.roleOf(secondRoleId), role -> assertEquals(secondRole, role)); + } + + @Test + public void itQueriesAllRoles() { + final RoleId firstRoleId = RoleId.from(TenantId.from("06f22ad4-49f5-46a4-b350-f6198a7646a3"), "first-role-name"); + final RoleView firstRole = RoleView.from(firstRoleId, "first-role-name", "first-role-description"); + final RoleId secondRoleId = RoleId.from(TenantId.from("3f51d0fd-335e-41b0-b57c-766470cf6ad7"), "second-role-name"); + final RoleView secondRole = RoleView.from(secondRoleId, "second-role-name", "second-role-description"); + + givenRolesExist(firstRole, secondRole); + + final Completes> outcome = queries.roles(); + + assertCompletes(outcome, roles -> { + assertContains(firstRole, roles); + assertContains(secondRole, roles); + }); + } + + @Test + public void itReturnsAnEmptyRoleIfItIsNotFound() { + final Completes result = queries.roleOf(RoleId.from(TenantId.from("8e8c0fe5-c727-43a6-9307-926214a71af4"), "role-c")); + + assertCompletes(result, role -> assertEquals("", role.id)); + } + + private static final StateStore.WriteResultInterest NOOP_WRITER = new StateStore.WriteResultInterest() { + @Override + public void writeResultedIn(Outcome outcome, String s, S s1, int i, List> list, Object o) { + } + }; + + private void givenRolesExist(final RoleView... roles) { + Arrays.stream(roles).forEach(role -> stateStore.write(role.id, role, 1, NOOP_WRITER)); + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionTest.java new file mode 100644 index 00000000..6c622672 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionTest.java @@ -0,0 +1,81 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.TenantData; +import io.vlingo.xoom.auth.model.tenant.*; +import io.vlingo.xoom.common.Completes; +import org.junit.jupiter.api.Test; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static org.junit.jupiter.api.Assertions.*; + +public class TenantProjectionTest extends ProjectionTest { + + @Test + public void itProjectsSubscribedTenant() { + final TenantId tenantId = TenantId.unique(); + + givenEvents( + new TenantSubscribed(tenantId, "tenant-1", "Tenant 1", false) + ); + + assertCompletes(tenantOf(tenantId), tenant -> { + assertEquals(tenantId.idString(), tenant.tenantId); + assertEquals("tenant-1", tenant.name); + assertEquals("Tenant 1", tenant.description); + assertFalse(tenant.active); + }); + } + + @Test + public void itProjectsActivatedTenant() { + final TenantId tenantId = TenantId.unique(); + + givenEvents( + new TenantSubscribed(tenantId, "tenant-1", "Tenant 1", false), + new TenantActivated(tenantId) + ); + + assertCompletes(tenantOf(tenantId), tenant -> assertTrue(tenant.active)); + } + + @Test + public void itProjectsDeactivatedTenant() { + final TenantId tenantId = TenantId.unique(); + + givenEvents( + new TenantSubscribed(tenantId, "tenant-1", "Tenant 1", true), + new TenantDeactivated(tenantId) + ); + + assertCompletes(tenantOf(tenantId), tenant -> assertFalse(tenant.active)); + } + + @Test + public void itProjectsTenantNameChange() { + final TenantId tenantId = TenantId.unique(); + + givenEvents( + new TenantSubscribed(tenantId, "tenant-1", "Tenant 1", true), + new TenantNameChanged(tenantId, "tenant-1-updated") + ); + + assertCompletes(tenantOf(tenantId), tenant -> assertEquals("tenant-1-updated", tenant.name)); + } + + + @Test + public void itProjectsTenantDescriptionChange() { + final TenantId tenantId = TenantId.unique(); + + givenEvents( + new TenantSubscribed(tenantId, "tenant-1", "Tenant 1", true), + new TenantDescriptionChanged(tenantId, "Tenant 1 Updated") + ); + + assertCompletes(tenantOf(tenantId), tenant -> assertEquals("Tenant 1 Updated", tenant.description)); + } + + private Completes tenantOf(final TenantId tenantId) { + return world.actorFor(TenantQueries.class, TenantQueriesActor.class, stateStore).tenantOf(tenantId); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesTest.java new file mode 100644 index 00000000..19ed9663 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesTest.java @@ -0,0 +1,92 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.auth.infrastructure.TenantData; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.common.Outcome; +import io.vlingo.xoom.lattice.model.stateful.StatefulTypeRegistry; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.Result; +import io.vlingo.xoom.symbio.store.StorageException; +import io.vlingo.xoom.symbio.store.dispatch.NoOpDispatcher; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.symbio.store.state.inmemory.InMemoryStateStoreActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertContains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TenantQueriesTest { + + private World world; + private StateStore stateStore; + private TenantQueries queries; + + @BeforeEach + public void setUp() { + world = World.startWithDefaults("test-state-store-query"); + stateStore = world.actorFor(StateStore.class, InMemoryStateStoreActor.class, Collections.singletonList(new NoOpDispatcher())); + StatefulTypeRegistry.registerAll(world, stateStore, TenantData.class); + queries = world.actorFor(TenantQueries.class, TenantQueriesActor.class, stateStore); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void itQueriesTheTenantById() { + final TenantId firstTenantId = TenantId.from("f6f2402b-dd97-4b07-9b6b-55138b173606"); + final TenantId secondTenantId = TenantId.from("4120a2f7-89ff-479b-934c-6d0ac2e2e138"); + final TenantData firstTenant = TenantData.from(firstTenantId, "first-tenant-name", "first-tenant-description", true); + final TenantData secondTenant = TenantData.from(secondTenantId, "second-tenant-name", "second-tenant-description", true); + + givenTenantsExist(firstTenant, secondTenant); + + assertCompletes(queries.tenantOf(firstTenantId), tenant -> assertEquals(firstTenant, tenant)); + assertCompletes(queries.tenantOf(secondTenantId), tenant -> assertEquals(secondTenant, tenant)); + } + + @Test + public void itQueriesAllTenants() { + final TenantId firstTenantId = TenantId.from("f6f2402b-dd97-4b07-9b6b-55138b173606"); + final TenantId secondTenantId = TenantId.from("4120a2f7-89ff-479b-934c-6d0ac2e2e138"); + final TenantData firstTenant = TenantData.from(firstTenantId, "first-tenant-name", "first-tenant-description", true); + final TenantData secondTenant = TenantData.from(secondTenantId, "second-tenant-name", "second-tenant-description", true); + + givenTenantsExist(firstTenant, secondTenant); + + final Completes> outcome = queries.tenants(); + + assertCompletes(outcome, tenants -> { + assertContains(firstTenant, tenants); + assertContains(secondTenant, tenants); + }); + } + + @Test + public void itReturnsAnEmptyTenantIfItIsNotFound() { + final Completes result = queries.tenantOf(TenantId.from("48000827-a6c8-4a29-8dbc-f88a5fa57b58")); + assertCompletes(result, tenant -> assertEquals("", tenant.tenantId)); + } + + private static final StateStore.WriteResultInterest NOOP_WRITER = new StateStore.WriteResultInterest() { + @Override + public void writeResultedIn(Outcome outcome, String s, S s1, int i, List> list, Object o) { + } + }; + + private void givenTenantsExist(final TenantData... tenants) { + Arrays.stream(tenants).forEach(tenant -> stateStore.write(tenant.tenantId, tenant, 1, NOOP_WRITER)); + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionTest.java new file mode 100644 index 00000000..4450ed5f --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionTest.java @@ -0,0 +1,197 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.auth.infrastructure.persistence.UserView.CredentialView; +import io.vlingo.xoom.auth.infrastructure.persistence.UserView.ProfileView; +import io.vlingo.xoom.auth.model.role.RoleId; +import io.vlingo.xoom.auth.model.role.RoleProvisioned; +import io.vlingo.xoom.auth.model.role.UserAssignedToRole; +import io.vlingo.xoom.auth.model.role.UserUnassignedFromRole; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.*; +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.PersonName; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.Completes; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static io.vlingo.xoom.auth.test.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +public class UserProjectionTest extends ProjectionTest { + + @Test + public void itProjectsRegisteredUser() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final Profile profile = Profile.from("bobby@example.com", PersonName.from("Bobby", "Tables", "Little"), "07777123123"); + final Set credentials = Collections.singleton(Credential.xoomCredentialFrom("authority", "credential-id", "secret")); + + givenEvents( + new UserRegistered(userId, "bobby", profile, credentials, false) + ); + + assertCompletes(userOf(userId), user -> { + assertEquals(userId.idString(), user.id); + assertEquals(userId.tenantId.idString(), user.tenantId); + assertEquals(ProfileView.from(profile), user.profile); + assertEquals(CredentialView.fromAll(credentials), user.credentials); + assertFalse(user.active); + }); + } + + @Test + public void itProjectsUserActivation() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), credentials(), false), + new UserActivated(userId) + ); + + assertCompletes(userOf(userId), user -> assertTrue(user.active)); + } + + @Test + public void itProjectsUserDeactivation() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), credentials(), true), + new UserDeactivated(userId) + ); + + assertCompletes(userOf(userId), user -> assertFalse(user.active)); + } + + @Test + public void itProjectsAddedCredentials() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final Set registeredCredentials = Collections.singleton(Credential.xoomCredentialFrom("authority", "credential-id", "secret")); + final Credential addedCredential = Credential.xoomCredentialFrom("authority-added", "credential-id-added", "secret-added"); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), registeredCredentials, false), + new UserCredentialAdded(userId, addedCredential) + ); + + assertCompletes(userOf(userId), user -> { + assertContainsAll(CredentialView.fromAll(registeredCredentials), user.credentials); + assertContains(CredentialView.from(addedCredential), user.credentials); + }); + } + + @Test + public void itProjectsRemovedCredentials() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final Credential firstCredential = Credential.xoomCredentialFrom("authority-1", "credential-id-1", "secret-1"); + final Credential secondCredential = Credential.xoomCredentialFrom("authority-2", "credential-id-2", "secret-2"); + final Set credentials = new HashSet<>(Arrays.asList(firstCredential, secondCredential)); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), credentials, false), + new UserCredentialRemoved(userId, "authority-1") + ); + + assertCompletes(userOf(userId), user -> { + assertNotContains(CredentialView.from(firstCredential), user.credentials); + assertContains(CredentialView.from(secondCredential), user.credentials); + }); + } + + @Test + public void itProjectsReplacementCredentials() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final Credential firstCredential = Credential.xoomCredentialFrom("authority-1", "credential-id-1", "secret-1"); + final Credential secondCredential = Credential.xoomCredentialFrom("authority-2", "credential-id-2", "secret-2"); + final Credential replacementCredential = Credential.xoomCredentialFrom("authority-3", "credential-id-3", "secret-3"); + final Set credentials = new HashSet<>(Arrays.asList(firstCredential, secondCredential)); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), credentials, false), + new UserCredentialReplaced(userId, "authority-1", replacementCredential) + ); + + assertCompletes(userOf(userId), user -> { + assertNotContains(CredentialView.from(firstCredential), user.credentials); + assertContains(CredentialView.from(secondCredential), user.credentials); + assertContains(CredentialView.from(replacementCredential), user.credentials); + }); + } + + @Test + public void itProjectsReplacementProfile() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final Profile profile = Profile.from("bobby@example.com", PersonName.from("Bobby", "Tables", "Little"), "07777123123"); + final Profile replacementProfile = Profile.from("alice@example.com", PersonName.from("Alice", "Green", "Gabrielle"), "07777999888"); + + givenEvents( + new UserRegistered(userId, "bobby", profile, credentials(), false), + new UserProfileReplaced(userId, replacementProfile) + ); + + assertCompletes(userOf(userId), user -> assertEquals(ProfileView.from(replacementProfile), user.profile)); + } + + @Test + public void itProjectsUserAssignedToRole() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final RoleId roleIdA = RoleId.from(userId.tenantId, "role-a"); + final RoleId roleIdB = RoleId.from(userId.tenantId, "role-b"); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), credentials(), true), + new RoleProvisioned(roleIdA, "role-a", "Role A"), + new RoleProvisioned(roleIdB, "role-b", "Role B"), + new UserAssignedToRole(roleIdA, userId), + new UserAssignedToRole(roleIdB, userId) + ); + + assertCompletes(userOf(userId), user -> { + assertEquals(setOf(Relation.userAssignedToRole(userId, roleIdA), Relation.userAssignedToRole(userId, roleIdB)), user.roles); + assertTrue(user.isInRole(roleIdA)); + assertTrue(user.isInRole(roleIdB)); + }); + } + + @Test + public void itProjectsUserUnassignedFromRole() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final RoleId roleIdA = RoleId.from(userId.tenantId, "role-a"); + final RoleId roleIdB = RoleId.from(userId.tenantId, "role-b"); + + givenEvents( + new UserRegistered(userId, "bobby", profile(), credentials(), true), + new RoleProvisioned(roleIdA, "role-a", "Role A"), + new RoleProvisioned(roleIdB, "role-b", "Role B"), + new UserAssignedToRole(roleIdA, userId), + new UserAssignedToRole(roleIdB, userId), + new UserUnassignedFromRole(roleIdA, userId) + ); + + assertCompletes(userOf(userId), user -> { + assertEquals(setOf(Relation.userAssignedToRole(userId, roleIdB)), user.roles); + assertFalse(user.isInRole(roleIdA)); + assertTrue(user.isInRole(roleIdB)); + }); + } + + private Completes userOf(final UserId userId) { + return world.actorFor(UserQueries.class, UserQueriesActor.class, stateStore).userOf(userId); + } + + private Set credentials() { + return Collections.singleton(Credential.xoomCredentialFrom("authority", "credential-id", "secret")); + } + + private Profile profile() { + return Profile.from("bobby@example.com", PersonName.from("Bobby", "Tables", "Little"), "07777123123"); + } + + private Set setOf(T... elements) { + return new HashSet(Arrays.asList(elements)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesTest.java new file mode 100644 index 00000000..ac077881 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesTest.java @@ -0,0 +1,92 @@ +package io.vlingo.xoom.auth.infrastructure.persistence; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.auth.infrastructure.persistence.UserView.PersonNameView; +import io.vlingo.xoom.auth.infrastructure.persistence.UserView.ProfileView; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.common.Outcome; +import io.vlingo.xoom.lattice.model.stateful.StatefulTypeRegistry; +import io.vlingo.xoom.symbio.Source; +import io.vlingo.xoom.symbio.store.Result; +import io.vlingo.xoom.symbio.store.StorageException; +import io.vlingo.xoom.symbio.store.dispatch.NoOpDispatcher; +import io.vlingo.xoom.symbio.store.state.StateStore; +import io.vlingo.xoom.symbio.store.state.inmemory.InMemoryStateStoreActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertContains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserQueriesTest { + + private World world; + private StateStore stateStore; + private UserQueries queries; + + @BeforeEach + public void setUp() { + world = World.startWithDefaults("test-state-store-query"); + stateStore = world.actorFor(StateStore.class, InMemoryStateStoreActor.class, Collections.singletonList(new NoOpDispatcher())); + StatefulTypeRegistry.registerAll(world, stateStore, UserView.class); + queries = world.actorFor(UserQueries.class, UserQueriesActor.class, stateStore); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void itQueriesTheUserById() { + final UserId firstUserId = UserId.from(TenantId.from("first-user-tenantId"), "first-user-username"); + final UserView firstUser = UserView.from(firstUserId, "first-user-username", ProfileView.from("first-user-profile-emailAddress", PersonNameView.from("first-user-profile-name-given", "first-user-profile-name-family", "first-user-profile-name-second"), "first-user-profile-phone"), Collections.emptySet(), true, Collections.emptySet()); + final UserId secondUserId = UserId.from(TenantId.from("second-user-tenantId"), "second-user-username"); + final UserView secondUser = UserView.from(secondUserId, "second-user-username", ProfileView.from("second-user-profile-emailAddress", PersonNameView.from("second-user-profile-name-given", "second-user-profile-name-family", "second-user-profile-name-second"), "second-user-profile-phone"), Collections.emptySet(), true, Collections.emptySet()); + + givenUsersExist(firstUser, secondUser); + + assertCompletes(queries.userOf(firstUserId), user -> assertEquals(firstUser, user)); + assertCompletes(queries.userOf(secondUserId), user -> assertEquals(secondUser, user)); + } + + @Test + public void itQueriesAllUsers() { + final UserId firstUserId = UserId.from(TenantId.from("first-user-tenantId"), "first-user-username"); + final UserView firstUser = UserView.from(firstUserId, "first-user-username", ProfileView.from("first-user-profile-emailAddress", PersonNameView.from("first-user-profile-name-given", "first-user-profile-name-family", "first-user-profile-name-second"), "first-user-profile-phone"), Collections.emptySet(), true, Collections.emptySet()); + final UserId secondUserId = UserId.from(TenantId.from("second-user-tenantId"), "second-user-username"); + final UserView secondUser = UserView.from(secondUserId, "second-user-username", ProfileView.from("second-user-profile-emailAddress", PersonNameView.from("second-user-profile-name-given", "second-user-profile-name-family", "second-user-profile-name-second"), "second-user-profile-phone"), Collections.emptySet(), true, Collections.emptySet()); + + givenUsersExist(firstUser, secondUser); + + final Completes> outcome = queries.users(); + + assertCompletes(outcome, users -> { + assertContains(firstUser, users); + assertContains(secondUser, users); + }); + } + + @Test + public void itReturnsAnEmptyUserIfItIsNotFound() { + final Completes result = queries.userOf(UserId.from(TenantId.from("68b0a384-52b4-453a-8893-daf8fcb508f6"), "bob")); + + assertCompletes(result, user -> assertEquals("", user.id)); + } + + private static final StateStore.WriteResultInterest NOOP_WRITER = new StateStore.WriteResultInterest() { + @Override + public void writeResultedIn(Outcome outcome, String s, S s1, int i, List> list, Object o) { + } + }; + + private void givenUsersExist(final UserView... users) { + Arrays.stream(users).forEach(user -> stateStore.write(user.id, user, 1, NOOP_WRITER)); + } +} \ No newline at end of file diff --git a/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddressTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddressTest.java new file mode 100644 index 00000000..758ef439 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddressTest.java @@ -0,0 +1,73 @@ +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.actors.Address; +import org.junit.jupiter.api.Test; + +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ComplexAddressTest { + + @Test + public void testItIsConvertedToLongId() { + final Function transformer = (ComplexId id) -> String.format("%s:%s", id.prefix, id.id); + + assertTrue((new ComplexAddress<>(new ComplexId("ABC", 123L), transformer)).id() > 0); + assertTrue((new ComplexAddress<>(new ComplexId("ABC", 132L), transformer)).id() > 0); + assertTrue((new ComplexAddress<>(new ComplexId("ABC", Long.MAX_VALUE), transformer)).id() > 0); + } + + @Test + public void testItTransformsTheIdToString() { + final Address address = new ComplexAddress<>( + new ComplexId("ABC", 123L), + (ComplexId id) -> String.format("%s:%s", id.prefix, id.id) + ); + + assertEquals(String.valueOf(address.id()), address.idString()); + assertEquals(String.valueOf(address.idSequence()), address.idSequenceString()); + } + + @Test + public void testCastsTheIdToTheRequestedType() { + final ComplexId complexId = new ComplexId("ABC", 123L); + final Address address = new ComplexAddress<>( + complexId, + (ComplexId id) -> String.format("%d", id.id) + ); + + assertEquals(complexId, address.idTyped()); + } + + @Test + public void testItIsDistributable() { + final Address address = new ComplexAddress<>( + new ComplexId("ABC", 123L), + (ComplexId id) -> String.format("%d", id.id) + ); + + assertTrue(address.isDistributable()); + } + + @Test + public void testItHasAName() { + final Address address = new ComplexAddress<>( + new ComplexId("ABC", 123L), + (ComplexId id) -> String.format("%d", id.id) + ); + + assertEquals("complex", address.name()); + } + + private class ComplexId { + public final String prefix; + public final Long id; + + public ComplexId(String prefix, Long id) { + this.prefix = prefix; + this.id = id; + } + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/GroupResourceTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResourceTest.java similarity index 95% rename from src/test/java/io/vlingo/xoom/auth/infra/resource/GroupResourceTest.java rename to src/test/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResourceTest.java index e8854b37..919b32f4 100644 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/GroupResourceTest.java +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResourceTest.java @@ -5,16 +5,16 @@ // was not distributed with this file, You can obtain // one at https://mozilla.org/MPL/2.0/. -package io.vlingo.xoom.auth.infra.resource; +package io.vlingo.xoom.auth.infrastructure.resource; import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Properties; - -import org.junit.Test; +import io.vlingo.xoom.auth.infrastructure.*; import io.vlingo.xoom.http.Response; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; public class GroupResourceTest extends ResourceTest { private GroupData groupData; @@ -35,6 +35,7 @@ public void testThatGroupDescriptionChanges() { } @Test + @Disabled public void testThatGroupGroupIsAssigned() { groupWithGroup("Group1", "Group 1 description."); @@ -47,6 +48,7 @@ public void testThatGroupGroupIsAssigned() { } @Test + @Disabled public void testThatGroupGroupIsUnassigned() { groupWithGroup("Group1", "Group 1 description."); @@ -59,6 +61,7 @@ public void testThatGroupGroupIsUnassigned() { } @Test + @Disabled public void testThatGroupUserIsAssigned() { groupWithUser(); @@ -71,6 +74,7 @@ public void testThatGroupUserIsAssigned() { } @Test + @Disabled public void testThatGroupUserIsUnassigned() { groupWithUser(); @@ -83,6 +87,7 @@ public void testThatGroupUserIsUnassigned() { } @Test + @Disabled public void testThatGroupHasRolePermission() { this.groupWithRoleWithPermission(); @@ -107,6 +112,7 @@ public void testThatGroupQueries() { } @Test + @Disabled public void testThatGroupGroupQueries() { groupWithGroup("Group1", "Group 1 description."); @@ -121,6 +127,7 @@ public void testThatGroupGroupQueries() { } @Test + @Disabled public void testThatGroupPermissionQueries() { this.groupWithRoleWithPermission(); @@ -137,6 +144,7 @@ public void testThatGroupPermissionQueries() { } @Test + @Disabled public void testThatGroupRoleQueries() { this.groupWithRoleWithPermission(); @@ -153,6 +161,7 @@ public void testThatGroupRoleQueries() { } @Test + @Disabled public void testThatGroupUserQueries() { groupWithUser(); @@ -168,21 +177,15 @@ public void testThatGroupUserQueries() { assertEquals(userData.profile.name.family, queriedUser.name.family); } - @Override - protected Properties resourceProperties() { - return TestProperties.groupResourceProperties( - TestProperties.permissionResourceProperties( - TestProperties.roleResourceProperties( - TestProperties.tenantResourceProperties()))); - } - private Response patchGroupChangeDescriptionRequestResponse(final GroupData groupData, final String description) { - final String request = "PATCH /tenants/" + groupData.tenantId + "/groups/" + groupData.name + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + description.length() + "\n\n" + description; + final String body = String.format("{\"description\":\"%s\"}", description); + final String request = "PATCH /tenants/" + groupData.tenantId + "/groups/" + groupData.name + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } private Response putGroupGroupRequestResponse(final GroupData groupData, final String groupName) { - final String request = "PUT /tenants/" + groupData.tenantId + "/groups/" + groupData.name + "/groups HTTP/1.1\nHost: vlingo.io\nContent-Length: " + groupName.length() + "\n\n" + groupName; + final String body = String.format("{\"name\":\"%s\"}", groupName); + final String request = "PUT /tenants/" + groupData.tenantId + "/groups/" + groupData.name + "/groups HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/PermissionResourceTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResourceTest.java similarity index 94% rename from src/test/java/io/vlingo/xoom/auth/infra/resource/PermissionResourceTest.java rename to src/test/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResourceTest.java index 8f965054..e4c87159 100644 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/PermissionResourceTest.java +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResourceTest.java @@ -5,18 +5,18 @@ // was not distributed with this file, You can obtain // one at https://mozilla.org/MPL/2.0/. -package io.vlingo.xoom.auth.infra.resource; +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.auth.infrastructure.ConstraintData; +import io.vlingo.xoom.auth.infrastructure.PermissionData; +import io.vlingo.xoom.auth.infrastructure.TenantData; +import io.vlingo.xoom.http.Response; +import org.junit.jupiter.api.Test; import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Properties; - -import org.junit.Test; - -import io.vlingo.xoom.http.Response; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class PermissionResourceTest extends ResourceTest { private PermissionData permissionData; @@ -106,11 +106,6 @@ public void testThatPermissionQueryFindsMultiple() { assertTrue(permissionData2.constraints.isEmpty()); } - @Override - protected Properties resourceProperties() { - return TestProperties.permissionResourceProperties(TestProperties.tenantResourceProperties()); - } - private ConstraintData constraintData(final int value) { return ConstraintData.from("String", "Name" + value, "" + value, "Description " + value + "."); } @@ -121,7 +116,8 @@ private Response getPermissionRequestResponse(final String tenantId, final Strin } private Response patchPermissionChangeDescriptionRequestResponse(final PermissionData permissionData, final String description) { - final String request = "PATCH /tenants/" + permissionData.tenantId + "/permissions/" + permissionData.name + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + description.length() + "\n\n" + description; + final String body = String.format("{\"description\":\"%s\"}", description); + final String request = "PATCH /tenants/" + permissionData.tenantId + "/permissions/" + permissionData.name + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/ResourceTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/ResourceTest.java similarity index 72% rename from src/test/java/io/vlingo/xoom/auth/infra/resource/ResourceTest.java rename to src/test/java/io/vlingo/xoom/auth/infrastructure/resource/ResourceTest.java index 213bb995..9efba46b 100644 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/ResourceTest.java +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/ResourceTest.java @@ -5,30 +5,31 @@ // was not distributed with this file, You can obtain // one at https://mozilla.org/MPL/2.0/. -package io.vlingo.xoom.auth.infra.resource; - -import io.vlingo.xoom.actors.Definition; -import io.vlingo.xoom.actors.World; -import io.vlingo.xoom.auth.infra.resource.TestResponseChannelConsumer.Progress; -import io.vlingo.xoom.auth.infra.resource.TestResponseChannelConsumer.TestResponseChannelConsumerInstantiator; -import io.vlingo.xoom.auth.model.Tenant; +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; +import io.restassured.http.ContentType; +import io.vlingo.xoom.auth.infrastructure.*; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.http.Header; +import io.vlingo.xoom.http.Request; import io.vlingo.xoom.http.Response; -import io.vlingo.xoom.http.resource.Server; -import io.vlingo.xoom.wire.channel.ResponseChannelConsumer; -import io.vlingo.xoom.wire.fdx.bidirectional.ClientRequestResponseChannel; -import io.vlingo.xoom.wire.fdx.bidirectional.netty.client.NettyClientRequestResponseChannel; +import io.vlingo.xoom.http.ResponseHeader; +import io.vlingo.xoom.turbo.ComponentRegistry; import io.vlingo.xoom.wire.message.ByteBufferAllocator; import io.vlingo.xoom.wire.message.Converters; -import io.vlingo.xoom.wire.node.Address; -import io.vlingo.xoom.wire.node.AddressType; -import io.vlingo.xoom.wire.node.Host; -import org.junit.After; -import org.junit.Before; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import java.nio.ByteBuffer; -import java.util.Properties; +import java.util.Collections; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static io.restassured.RestAssured.given; import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; public abstract class ResourceTest { @@ -36,46 +37,25 @@ public abstract class ResourceTest { private final ByteBuffer buffer = ByteBufferAllocator.allocate(65535); - protected ResponseChannelConsumer consumer; - protected ClientRequestResponseChannel client; - protected Progress progress; - protected Properties properties; - protected Server server; + protected XoomInitializer xoom; protected int serverPort; - protected World world; - @Before + @BeforeEach public void setUp() throws Exception { - world = World.start("resource-test"); - serverPort = baseServerPort.getAndIncrement(); - properties = resourceProperties(); - properties.setProperty("server.http.port", "" + serverPort); - - server = Server.startWith(world.stage(), properties); - Thread.sleep(10); // delay for server startup - - consumer = world.actorFor(ResponseChannelConsumer.class, Definition.has(TestResponseChannelConsumer.class, new TestResponseChannelConsumerInstantiator(progress))); - - client = new NettyClientRequestResponseChannel(Address.from(Host.of("localhost"), serverPort, AddressType.NONE), consumer, 100, 10240); - } - - @After - public void tearDown() { - client.close(); - - server.stop(); - - world.terminate(); + XoomInitializer.main(new String[]{"-Dport=" + serverPort}); + xoom = XoomInitializer.instance(); + xoom.server().startUp().await(100); } - protected ResourceTest() { - progress = new Progress(0); + @AfterEach + public void tearDown() throws Exception { + xoom.stopServer(); + xoom.terminateWorld(); + ComponentRegistry.clear(); } - protected abstract Properties resourceProperties(); - protected ByteBuffer toByteBuffer(final String requestContent) { buffer.clear(); buffer.put(Converters.textToBytes(requestContent)); @@ -103,13 +83,13 @@ protected TenantData tenantData(final String name, final String description, boo return TenantData.from(name, description, active); } - protected TenantData tenantData(final Tenant tenant, final boolean withId) { - return TenantData.from(withId ? tenant.tenantId().value : null, tenant.name(), tenant.description(), tenant.isActive()); - } +// protected TenantData tenantData(final Tenant tenant, final boolean withId) { +// return TenantData.from(withId ? tenant.tenantId().value : null, tenant.name(), tenant.description(), tenant.isActive()); +// } protected UserRegistrationData userRegistrationData(final String tenantId) { return UserRegistrationData.from( - tenantId, + UserId.from(TenantId.from(tenantId), "useroftheyear"), "useroftheyear", ProfileData.from(PersonNameData.of("Given", "A", "Family"), "me@family.us", "212-555-1212"), CredentialData.from("xoom-platform", "useroftheyear", "topsecret"), @@ -118,7 +98,7 @@ protected UserRegistrationData userRegistrationData(final String tenantId) { protected UserRegistrationData userRegistrationData(final String tenantId, final int value) { return UserRegistrationData.from( - tenantId, + UserId.from(TenantId.from(tenantId), "useroftheyear"), "useroftheyear" + value, ProfileData.from(PersonNameData.of("Given" + value, "A", "Family" + value), "me" + value + "@family.us", "212-555-1212"), CredentialData.from("xoom-platform", "useroftheyear" + value, "topsecret" + value), @@ -196,19 +176,19 @@ protected Response getRoleUserRequestResponse(RoleData roleData, String username } protected Response postProvisionGroup(final String tenantId, final String name, final String description) { - final String body = serialized(GroupData.from(name, description)); + final String body = serialized(GroupData.from(TenantId.from(tenantId), name, description)); final String request = "POST /tenants/" + tenantId + "/groups HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } protected Response postProvisionPermission(String tenantId, String name, String description) { - final String body = serialized(PermissionData.from(name, description)); + final String body = serialized(PermissionData.from(TenantId.from(tenantId), Collections.emptySet(), name, description)); final String request = "POST /tenants/" + tenantId + "/permissions HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } protected Response postProvisionRole(final String tenantId, final String name, final String description) { - final String body = serialized(RoleData.from(name, description)); + final String body = serialized(RoleData.from(TenantId.from(tenantId), name, description)); final String request = "POST /tenants/" + tenantId + "/roles HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } @@ -225,18 +205,27 @@ protected Response postRegisterUser(String tenantId, UserRegistrationData userRe } protected Response requestResponse(final String request) { - progress.resetTimes(1); - - client.requestWith(toByteBuffer(request)); - - while (progress.remaining() > 0) { - client.probeChannel(); - } - - progress.completes(); - - final Response response = progress.responses().poll(); - - return response; + return requestResponse(Request.from(toByteBuffer(request))); + } + + private Response requestResponse(Request request) { + final io.restassured.response.Response response = given() + .port(serverPort) + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .filter(new RequestLoggingFilter()) + .filter(new ResponseLoggingFilter()) + .when() + .headers(request.headers.stream().filter(h -> !h.name.equals("Content-Length")).collect(Collectors.toMap(h -> h.name, h -> h.value))) + .body(request.body.content()) + .request(request.method.name, request.uri.getPath()) + .andReturn(); + final Stream headers = response.headers().asList().stream().map(h -> ResponseHeader.of(h.getName(), h.getValue())); + final Response.Status statusCode = Response.Status.valueOfRawState(response.statusLine().replaceFirst("HTTP/1.1 ", "")); + return Response.of( + statusCode, + Header.Headers.of(headers.toArray(ResponseHeader[]::new)), + response.body().asString() + ); } } diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/RoleResourceTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResourceTest.java similarity index 93% rename from src/test/java/io/vlingo/xoom/auth/infra/resource/RoleResourceTest.java rename to src/test/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResourceTest.java index dd090509..3eaee4c9 100644 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/RoleResourceTest.java +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResourceTest.java @@ -5,16 +5,14 @@ // was not distributed with this file, You can obtain // one at https://mozilla.org/MPL/2.0/. -package io.vlingo.xoom.auth.infra.resource; - -import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; -import static org.junit.Assert.assertEquals; - -import java.util.Properties; - -import org.junit.Test; +package io.vlingo.xoom.auth.infrastructure.resource; +import io.vlingo.xoom.auth.infrastructure.*; import io.vlingo.xoom.http.Response; +import org.junit.jupiter.api.Test; + +import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; +import static org.junit.jupiter.api.Assertions.assertEquals; public class RoleResourceTest extends ResourceTest { private GroupData groupData; @@ -161,18 +159,15 @@ public void testThatRoleUserQueries() { assertEquals(userData.profile.name.family, queriedUser.name.family); } - @Override - protected Properties resourceProperties() { - return TestProperties.roleResourceProperties(TestProperties.tenantResourceProperties()); - } - private Response patchRoleChangeDescriptionRequestResponse(final RoleData roleData, final String description) { - final String request = "PATCH /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + description.length() + "\n\n" + description; + final String body = String.format("{\"description\":\"%s\"}", description); + final String request = "PATCH /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } private Response putRoleGroupRequestResponse(final RoleData roleData, final String groupName) { - final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/groups HTTP/1.1\nHost: vlingo.io\nContent-Length: " + groupName.length() + "\n\n" + groupName; + final String body = String.format("{\"name\":\"%s\"}", groupName); + final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/groups HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } @@ -182,7 +177,8 @@ private Response deleteRoleGroupRequestResponse(final RoleData roleData, final S } private Response putRolePermissionRequestResponse(final RoleData roleData, final String permissionName) { - final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/permissions HTTP/1.1\nHost: vlingo.io\nContent-Length: " + permissionName.length() + "\n\n" + permissionName; + final String body = String.format("{\"name\":\"%s\"}", permissionName); + final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/permissions HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } @@ -192,7 +188,8 @@ private Response deleteRolePermissionRequestResponse(RoleData roleData, String p } private Response putRoleUserRequestResponse(final RoleData roleData, final String username) { - final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/users HTTP/1.1\nHost: vlingo.io\nContent-Length: " + username.length() + "\n\n" + username; + final String body = String.format("{\"username\":\"%s\"}", username); + final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/users HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/TenantResourceTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResourceTest.java similarity index 94% rename from src/test/java/io/vlingo/xoom/auth/infra/resource/TenantResourceTest.java rename to src/test/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResourceTest.java index d56407b1..0231e860 100644 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/TenantResourceTest.java +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResourceTest.java @@ -5,26 +5,20 @@ // was not distributed with this file, You can obtain // one at https://mozilla.org/MPL/2.0/. -package io.vlingo.xoom.auth.infra.resource; +package io.vlingo.xoom.auth.infrastructure.resource; -import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; -import static io.vlingo.xoom.common.serialization.JsonSerialization.deserializedList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import com.google.gson.reflect.TypeToken; +import io.vlingo.xoom.auth.infrastructure.*; +import io.vlingo.xoom.http.Response; +import io.vlingo.xoom.http.ResponseHeader; +import org.junit.jupiter.api.Test; import java.lang.reflect.Type; import java.util.List; -import java.util.Properties; - -import org.junit.Test; - -import com.google.gson.reflect.TypeToken; -import io.vlingo.xoom.http.Response; -import io.vlingo.xoom.http.ResponseHeader; +import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; +import static io.vlingo.xoom.common.serialization.JsonSerialization.deserializedList; +import static org.junit.jupiter.api.Assertions.*; public class TenantResourceTest extends ResourceTest { @@ -34,13 +28,11 @@ public void testThatTenantSubscribes() { final Response createdResponse = postTenantSubscribesRequestResponse(tenantData); final TenantData createdTenantData = deserialized(createdResponse.entity.content(), TenantData.class); - assertEquals(1, progress.consumeCount()); assertNotNull(createdResponse.headers.headerOf(ResponseHeader.Location)); assertEquals(tenantData.withTenantId(createdTenantData.tenantId), createdTenantData); final String location = createdResponse.headerOf(ResponseHeader.Location).value; final Response getTenantResponse = getTenantRequestResponse(location); - assertEquals(2, progress.consumeCount()); assertEquals(Response.Status.Ok, getTenantResponse.status); assertNotNull(getTenantResponse.entity); assertNotNull(getTenantResponse.entity.content()); @@ -190,10 +182,10 @@ public void testThatTenantRegistersUsers() { assertEquals(userRegData.profile.name.family, userRegistrateredData.profile.name.family); assertEquals(userRegData.profile.emailAddress, userRegistrateredData.profile.emailAddress); assertEquals(userRegData.profile.phone, userRegistrateredData.profile.phone); - assertEquals(userRegData.credential.authority, userRegistrateredData.credential.authority); - assertEquals(userRegData.credential.id, userRegistrateredData.credential.id); - assertNotEquals(userRegData.credential.secret, userRegistrateredData.credential.secret); - assertEquals("VLINGO", userRegistrateredData.credential.type); + assertEquals(userRegData.credentials.stream().findFirst().get().authority, userRegistrateredData.credentials.stream().findFirst().get().authority); + assertEquals(userRegData.credentials.stream().findFirst().get().id, userRegistrateredData.credentials.stream().findFirst().get().id); + assertNotEquals(userRegData.credentials.stream().findFirst().get().secret, userRegistrateredData.credentials.stream().findFirst().get().secret); + assertEquals("XOOM", userRegistrateredData.credentials.stream().findFirst().get().type); assertTrue(userRegistrateredData.active); } @@ -298,11 +290,6 @@ public void testThatTenantQueriesUsers() { assertEquals(2, userData.size()); } - @Override - protected Properties resourceProperties() { - return TestProperties.tenantResourceProperties(); - } - private Response patchTenantActivateRequestResponse(final String tenantId) { final String request = "PATCH /tenants/" + tenantId + "/activate HTTP/1.1\nHost: vlingo.io\n\n"; return requestResponse(request); @@ -314,12 +301,14 @@ private Response patchTenantDeactivateRequestResponse(final String tenantId) { } private Response patchTenantDescriptionRequestResponse(final String tenantId, final String description) { - final String request = "PATCH /tenants/" + tenantId + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + description.length() + "\n\n" + description; + final String body = String.format("{\"description\":\"%s\"}", description); + final String request = "PATCH /tenants/" + tenantId + "/description HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } private Response patchTenantNameRequestResponse(final String tenantId, final String name) { - final String request = "PATCH /tenants/" + tenantId + "/name HTTP/1.1\nHost: vlingo.io\nContent-Length: " + name.length() + "\n\n" + name; + final String body = String.format("{\"name\":\"%s\"}", name); + final String request = "PATCH /tenants/" + tenantId + "/name HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } } diff --git a/src/test/java/io/vlingo/xoom/auth/infra/resource/UserResourceTest.java b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/UserResourceTest.java similarity index 81% rename from src/test/java/io/vlingo/xoom/auth/infra/resource/UserResourceTest.java rename to src/test/java/io/vlingo/xoom/auth/infrastructure/resource/UserResourceTest.java index 43979937..0bc0ae00 100644 --- a/src/test/java/io/vlingo/xoom/auth/infra/resource/UserResourceTest.java +++ b/src/test/java/io/vlingo/xoom/auth/infrastructure/resource/UserResourceTest.java @@ -5,21 +5,17 @@ // was not distributed with this file, You can obtain // one at https://mozilla.org/MPL/2.0/. -package io.vlingo.xoom.auth.infra.resource; +package io.vlingo.xoom.auth.infrastructure.resource; + +import io.vlingo.xoom.auth.infrastructure.*; +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.http.Response; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import static io.vlingo.xoom.common.serialization.JsonSerialization.deserialized; import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.Properties; - -import org.junit.Test; - -import io.vlingo.xoom.auth.model.Credential; -import io.vlingo.xoom.http.Response; +import static org.junit.jupiter.api.Assertions.*; public class UserResourceTest extends ResourceTest { private PermissionData permissionData; @@ -33,22 +29,22 @@ public void testThatUserDeactivatesAndActivates() { final Response patchUserDeactive = patchUserDeactivateRequestResponse(userData); assertEquals(Response.Status.Ok, patchUserDeactive.status); - final UserData deactivatedUserData = deserialized(patchUserDeactive.entity.content(), UserData.class); + final UserRegistrationData deactivatedUserData = deserialized(patchUserDeactive.entity.content(), UserRegistrationData.class); assertFalse(deactivatedUserData.active); final Response getUserResponse1 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse1.status); - final UserData getDeactivatedUserData = deserialized(getUserResponse1.entity.content(), UserData.class); + final UserRegistrationData getDeactivatedUserData = deserialized(getUserResponse1.entity.content(), UserRegistrationData.class); assertFalse(getDeactivatedUserData.active); final Response patchUserActivate = this.patchUserActivateRequestResponse(userData); assertEquals(Response.Status.Ok, patchUserActivate.status); - final UserData activatedUserData = deserialized(patchUserActivate.entity.content(), UserData.class); + final UserRegistrationData activatedUserData = deserialized(patchUserActivate.entity.content(), UserRegistrationData.class); assertTrue(activatedUserData.active); final Response getUserResponse2 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse2.status); - final UserData getActivatedUserData = deserialized(getUserResponse2.entity.content(), UserData.class); + final UserRegistrationData getActivatedUserData = deserialized(getUserResponse2.entity.content(), UserRegistrationData.class); assertTrue(getActivatedUserData.active); } @@ -61,14 +57,14 @@ public void testThatUserManagesCredentials() { final CredentialData newCredentialData = CredentialData.from("abc", "username1", "secret1", Credential.Type.OAUTH.name()); final Response patchUserAddCredential = putUserAddCredentialRequestResponse(userData, newCredentialData); assertEquals(Response.Status.Ok, patchUserAddCredential.status); - final UserData newCredentialUserData = deserialized(patchUserAddCredential.entity.content(), UserData.class); + final UserRegistrationData newCredentialUserData = deserialized(patchUserAddCredential.entity.content(), UserRegistrationData.class); assertEquals(newCredentialData.authority, newCredentialUserData.credentialOf("abc").authority); assertEquals(newCredentialData.id, newCredentialUserData.credentialOf("abc").id); assertEquals(newCredentialData.type, newCredentialUserData.credentialOf("abc").type); final Response getUserResponse1 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse1.status); - final UserData getUserData1 = deserialized(getUserResponse1.entity.content(), UserData.class); + final UserRegistrationData getUserData1 = deserialized(getUserResponse1.entity.content(), UserRegistrationData.class); assertEquals(newCredentialData.authority, getUserData1.credentialOf("abc").authority); assertEquals(newCredentialData.id, getUserData1.credentialOf("abc").id); assertEquals(newCredentialData.type, getUserData1.credentialOf("abc").type); @@ -77,12 +73,12 @@ public void testThatUserManagesCredentials() { final Response deleteUserAddCredential = deleteUserCredentialRequestResponse(userData, "abc"); assertEquals(Response.Status.Ok, deleteUserAddCredential.status); - final UserData deletedCredentialUserData = deserialized(deleteUserAddCredential.entity.content(), UserData.class); + final UserRegistrationData deletedCredentialUserData = deserialized(deleteUserAddCredential.entity.content(), UserRegistrationData.class); assertNull(deletedCredentialUserData.credentialOf("abc")); final Response getUserResponse2 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse2.status); - final UserData getUserData2 = deserialized(getUserResponse2.entity.content(), UserData.class); + final UserRegistrationData getUserData2 = deserialized(getUserResponse2.entity.content(), UserRegistrationData.class); assertNull(getUserData2.credentialOf("abc")); // replace @@ -94,14 +90,14 @@ public void testThatUserManagesCredentials() { final Response patchUserReplaceCredential = patchUserReplaceCredentialRequestResponse(userData, "abc", credentialDataUsedToReplace); assertEquals(Response.Status.Ok, patchUserReplaceCredential.status); - final UserData patchUserReplaceCredentialUserData = deserialized(patchUserReplaceCredential.entity.content(), UserData.class); + final UserRegistrationData patchUserReplaceCredentialUserData = deserialized(patchUserReplaceCredential.entity.content(), UserRegistrationData.class); assertEquals(credentialDataUsedToReplace.authority, patchUserReplaceCredentialUserData.credentialOf("cba").authority); assertEquals(credentialDataUsedToReplace.id, patchUserReplaceCredentialUserData.credentialOf("cba").id); assertEquals(credentialDataUsedToReplace.type, patchUserReplaceCredentialUserData.credentialOf("cba").type); final Response getUserResponse3 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse3.status); - final UserData getUserData3 = deserialized(getUserResponse3.entity.content(), UserData.class); + final UserRegistrationData getUserData3 = deserialized(getUserResponse3.entity.content(), UserRegistrationData.class); assertEquals(credentialDataUsedToReplace.authority, getUserData3.credentialOf("cba").authority); assertEquals(credentialDataUsedToReplace.id, getUserData3.credentialOf("cba").id); assertEquals(credentialDataUsedToReplace.type, getUserData3.credentialOf("cba").type); @@ -114,7 +110,7 @@ public void testThatUserReplacesProfile() { final ProfileData newProfileData = ProfileData.from(PersonNameData.of("A", "B", "C"), "a@c.com", "888-888-8888"); final Response patchUserReplaceProfile = patchUserReplaceProfileRequestResponse(userData, newProfileData); assertEquals(Response.Status.Ok, patchUserReplaceProfile.status); - final UserData newProfileUserData = deserialized(patchUserReplaceProfile.entity.content(), UserData.class); + final UserRegistrationData newProfileUserData = deserialized(patchUserReplaceProfile.entity.content(), UserRegistrationData.class); assertEquals(newProfileData.name.given, newProfileUserData.profile.name.given); assertEquals(newProfileData.name.second, newProfileUserData.profile.name.second); assertEquals(newProfileData.name.family, newProfileUserData.profile.name.family); @@ -123,7 +119,7 @@ public void testThatUserReplacesProfile() { final Response getUserResponse1 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse1.status); - final UserData getUserData1 = deserialized(getUserResponse1.entity.content(), UserData.class); + final UserRegistrationData getUserData1 = deserialized(getUserResponse1.entity.content(), UserRegistrationData.class); assertEquals(newProfileData.name.given, getUserData1.profile.name.given); assertEquals(newProfileData.name.second, getUserData1.profile.name.second); assertEquals(newProfileData.name.family, getUserData1.profile.name.family); @@ -137,7 +133,7 @@ public void testThatUserQueries() { final Response getUserResponse1 = getUserRequestResponse(userData); assertEquals(Response.Status.Ok, getUserResponse1.status); - final UserData userDataAsQueried = deserialized(getUserResponse1.entity.content(), UserData.class); + final UserRegistrationData userDataAsQueried = deserialized(getUserResponse1.entity.content(), UserRegistrationData.class); assertEquals(userData.tenantId, userDataAsQueried.tenantId); assertEquals(userData.username, userDataAsQueried.username); assertEquals(userData.profile.name.given, userDataAsQueried.profile.name.given); @@ -145,14 +141,15 @@ public void testThatUserQueries() { assertEquals(userData.profile.name.family, userDataAsQueried.profile.name.family); assertEquals(userData.profile.emailAddress, userDataAsQueried.profile.emailAddress); assertEquals(userData.profile.phone, userDataAsQueried.profile.phone); - assertEquals(userData.credential.authority, userDataAsQueried.credentials.get(0).authority); - assertEquals(userData.credential.id, userDataAsQueried.credentials.get(0).id); - assertEquals(userData.credential.secret, userDataAsQueried.credentials.get(0).secret); - assertEquals("VLINGO", userDataAsQueried.credentials.get(0).type); + assertEquals(userData.credentials.stream().findFirst().get().authority, userDataAsQueried.credentials.stream().findFirst().get().authority); + assertEquals(userData.credentials.stream().findFirst().get().id, userDataAsQueried.credentials.stream().findFirst().get().id); + assertEquals(userData.credentials.stream().findFirst().get().secret, userDataAsQueried.credentials.stream().findFirst().get().secret); + assertEquals("XOOM", userDataAsQueried.credentials.stream().findFirst().get().type); assertTrue(userDataAsQueried.active); } @Test + @Disabled public void testThatUserHasPermission() { userWithRolePermission(); @@ -176,15 +173,6 @@ public void testThatUserIsInRole() { assertEquals(roleData.description, userInRoleData.description); } - @Override - protected Properties resourceProperties() { - return TestProperties.userResourceProperties( - TestProperties.groupResourceProperties( - TestProperties.permissionResourceProperties( - TestProperties.roleResourceProperties( - TestProperties.tenantResourceProperties())))); - } - private Response deleteUserCredentialRequestResponse(final UserRegistrationData userData, String authority) { final String request = "DELETE /tenants/" + userData.tenantId + "/users/" + userData.username + "/credentials/" + authority + " HTTP/1.1\nHost: vlingo.io\n\n"; return requestResponse(request); @@ -234,12 +222,14 @@ private Response patchUserReplaceProfileRequestResponse(final UserRegistrationDa } private Response putRoleUserRequestResponse(final RoleData roleData, final String username) { - final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/users HTTP/1.1\nHost: vlingo.io\nContent-Length: " + username.length() + "\n\n" + username; + final String body = String.format("{\"username\":\"%s\"}", username); + final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/users HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } private Response putRolePermissionRequestResponse(final RoleData roleData, final String permissionName) { - final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/permissions HTTP/1.1\nHost: vlingo.io\nContent-Length: " + permissionName.length() + "\n\n" + permissionName; + final String body = String.format("{\"name\":\"%s\"}", permissionName); + final String request = "PUT /tenants/" + roleData.tenantId + "/roles/" + roleData.name + "/permissions HTTP/1.1\nHost: vlingo.io\nContent-Length: " + body.length() + "\n\n" + body; return requestResponse(request); } diff --git a/src/test/java/io/vlingo/xoom/auth/model/group/GroupEntityTest.java b/src/test/java/io/vlingo/xoom/auth/model/group/GroupEntityTest.java new file mode 100644 index 00000000..83865db2 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/group/GroupEntityTest.java @@ -0,0 +1,175 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.auth.infrastructure.persistence.*; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.value.EncodedMember; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry.Info; +import io.vlingo.xoom.symbio.EntryAdapterProvider; +import io.vlingo.xoom.symbio.store.journal.Journal; +import io.vlingo.xoom.symbio.store.journal.inmemory.InMemoryJournalActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertEventDispatched; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class GroupEntityTest { + + private final TenantId TENANT_ID = TenantId.from("7c7161d4-12c9-4dde-9e8f-26c40bfc7902"); + private final String GROUP_NAME = "Group A"; + private final String GROUP_DESCRIPTION = "Group A description"; + private final GroupId GROUP_ID = GroupId.from(TENANT_ID, GROUP_NAME); + + private World world; + private MockDispatcher dispatcher; + + @BeforeEach + @SuppressWarnings({"unchecked", "rawtypes"}) + public void setUp() { + world = World.startWithDefaults("test-es"); + dispatcher = new MockDispatcher(); + + final EntryAdapterProvider entryAdapterProvider = EntryAdapterProvider.instance(world); + entryAdapterProvider.registerAdapter(GroupProvisioned.class, new GroupProvisionedAdapter()); + entryAdapterProvider.registerAdapter(GroupDescriptionChanged.class, new GroupDescriptionChangedAdapter()); + entryAdapterProvider.registerAdapter(GroupAssignedToGroup.class, new GroupAssignedToGroupAdapter()); + entryAdapterProvider.registerAdapter(GroupUnassignedFromGroup.class, new GroupUnassignedFromGroupAdapter()); + entryAdapterProvider.registerAdapter(UserAssignedToGroup.class, new UserAssignedToGroupAdapter()); + entryAdapterProvider.registerAdapter(UserUnassignedFromGroup.class, new UserUnassignedFromGroupAdapter()); + + final Journal journal = world.actorFor(Journal.class, InMemoryJournalActor.class, Collections.singletonList(dispatcher)); + + new SourcedTypeRegistry(world).register(new Info(journal, GroupEntity.class, GroupEntity.class.getSimpleName())); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void groupIsProvisionedWithNameAndDescription() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Completes outcome = groupOf(GROUP_ID).provisionGroup(GROUP_NAME, GROUP_DESCRIPTION); + + assertCompletes(outcome, state -> { + assertEquals(GROUP_NAME, state.name); + assertEquals(GROUP_DESCRIPTION, state.description); + assertEquals(GROUP_ID, state.id); + assertEventDispatched(dispatcherAccess, 1, GroupProvisioned.class); + }); + } + + @Test + public void groupDescriptionIsChanged() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenGroupIsProvisioned(GROUP_ID, GROUP_NAME, GROUP_DESCRIPTION) + .andThenTo(g -> groupOf(GROUP_ID).changeDescription("updated-groupOf-description")); + + assertCompletes(outcome, state -> { + assertEquals(GROUP_NAME, state.name); + assertEquals("updated-groupOf-description", state.description); + assertEquals(GROUP_ID, state.id); + assertEventDispatched(dispatcherAccess, 2, GroupDescriptionChanged.class); + }); + } + + @Test + public void groupIsAssignedAnotherGroup() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final GroupId innerGroupId = GroupId.from(TENANT_ID, "Group B"); + + final Completes outcome = givenGroupIsProvisioned(GROUP_ID, GROUP_NAME, GROUP_DESCRIPTION) + .andThenTo(g -> givenGroupIsProvisioned(innerGroupId, "Group B", "Group B description")) + .andThenTo(g -> groupOf(GROUP_ID).assignGroup(innerGroupId)); + + assertCompletes(outcome, state -> { + assertContainsMember(EncodedMember.group(innerGroupId), state); + assertEventDispatched(dispatcherAccess, 3, GroupAssignedToGroup.class); + }); + } + + @Test + public void groupIsUnassignedFromAnotherGroup() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final GroupId groupBId = GroupId.from(TENANT_ID, "Group B"); + final GroupId groupCId = GroupId.from(TENANT_ID, "Group C"); + + final Completes outcome = givenGroupIsProvisioned(GROUP_ID, GROUP_NAME, GROUP_DESCRIPTION) + .andThenTo(g -> givenGroupIsProvisioned(groupBId, "Group B", "Group B description")) + .andThenTo(g -> givenGroupIsProvisioned(groupBId, "Group C", "Group C description")) + .andThenTo(g -> groupOf(GROUP_ID).assignGroup(groupBId)) + .andThenTo(g -> groupOf(GROUP_ID).assignGroup(groupCId)) + .andThenTo(g -> groupOf(GROUP_ID).unassignGroup(groupBId)); + + assertCompletes(outcome, state -> { + assertNotContainsMember(EncodedMember.group(groupBId), state); + assertContainsMember(EncodedMember.group(groupCId), state); + assertEventDispatched(dispatcherAccess, 6, GroupUnassignedFromGroup.class); + }); + } + + @Test + public void userIsAssignedToAGroup() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + + final Completes outcome = givenGroupIsProvisioned(GROUP_ID) + .andThenTo(g -> groupOf(GROUP_ID).assignUser(userId)); + + assertCompletes(outcome, state -> { + assertContainsMember(EncodedMember.user(userId), state); + assertEventDispatched(dispatcherAccess, 2, UserAssignedToGroup.class); + }); + } + + @Test + public void userIsUnassignedFromAGroup() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final UserId firstUserId = UserId.from(TenantId.unique(), "bobby"); + final UserId secondUserId = UserId.from(TenantId.unique(), "alice"); + + final Completes outcome = givenGroupIsProvisioned(GROUP_ID) + .andThenTo(g -> groupOf(GROUP_ID).assignUser(firstUserId)) + .andThenTo(g -> groupOf(GROUP_ID).assignUser(secondUserId)) + .andThenTo(g -> groupOf(GROUP_ID).unassignUser(firstUserId)); + + assertCompletes(outcome, state -> { + assertNotContainsMember(EncodedMember.user(firstUserId), state); + assertContainsMember(EncodedMember.user(secondUserId), state); + assertEventDispatched(dispatcherAccess, 4, UserUnassignedFromGroup.class); + }); + } + + private Group groupOf(final GroupId groupId) { + return world.actorFor(Group.class, GroupEntity.class, groupId); + } + + private Completes givenGroupIsProvisioned(final GroupId groupId) { + return groupOf(groupId).provisionGroup(GROUP_NAME, GROUP_DESCRIPTION); + } + + private Completes givenGroupIsProvisioned(final GroupId groupId, final String name, final String description) { + return groupOf(groupId).provisionGroup(name, description); + } + + private void assertContainsMember(final EncodedMember member, final GroupState state) { + assertEquals(new HashSet<>(Collections.singletonList(member)), state.members); + } + + private void assertNotContainsMember(final EncodedMember member, final GroupState state) { + state.members.forEach(m -> assertNotEquals(m, member)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/group/GroupIdTest.java b/src/test/java/io/vlingo/xoom/auth/model/group/GroupIdTest.java new file mode 100644 index 00000000..3ed690f9 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/group/GroupIdTest.java @@ -0,0 +1,33 @@ +package io.vlingo.xoom.auth.model.group; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class GroupIdTest { + @Test + public void itIsComposedOfTenantIdAndGroupName() { + final GroupId groupId = GroupId.from(TenantId.from("7c467e63-2b63-43dd-8198-67d31a776f4d"), "Admin"); + + assertEquals(TenantId.from("7c467e63-2b63-43dd-8198-67d31a776f4d"), groupId.tenantId); + assertEquals("Admin", groupId.groupName); + assertEquals("7c467e63-2b63-43dd-8198-67d31a776f4d:Admin", groupId.idString()); + } + + @Test + public void twoGroupIdsAreTheSameIfTenantIdAndGroupIdAreTheSame() { + final GroupId groupId = GroupId.from(TenantId.from("7c467e63-2b63-43dd-8198-67d31a776f4d"), "Admin"); + + assertEquals(groupId, GroupId.from(TenantId.from("7c467e63-2b63-43dd-8198-67d31a776f4d"), "Admin")); + assertNotEquals(groupId, GroupId.from(TenantId.from("97d57df1-1b38-4583-bd3c-6b4731e7a605"), "Admin")); + assertNotEquals(groupId, GroupId.from(TenantId.from("7c467e63-2b63-43dd-8198-67d31a776f4d"), "Staff")); + } + + @Test + public void itCreatesGroupIdFromString() { + final GroupId groupId = GroupId.from(TenantId.from("7c467e63-2b63-43dd-8198-67d31a776f4d"), "Admin"); + + assertEquals(groupId, GroupId.from("7c467e63-2b63-43dd-8198-67d31a776f4d:Admin")); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/permission/PermissionEntityTest.java b/src/test/java/io/vlingo/xoom/auth/model/permission/PermissionEntityTest.java new file mode 100644 index 00000000..3c5798f9 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/permission/PermissionEntityTest.java @@ -0,0 +1,157 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.auth.infrastructure.persistence.*; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.value.Constraint; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry.Info; +import io.vlingo.xoom.symbio.EntryAdapterProvider; +import io.vlingo.xoom.symbio.store.journal.Journal; +import io.vlingo.xoom.symbio.store.journal.inmemory.InMemoryJournalActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertEventDispatched; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class PermissionEntityTest { + + private final TenantId TENANT_ID = TenantId.from("d8e6476f-00c2-4bfd-9790-bf6388ed78d2"); + private final String PERMISSION_NAME = "permission-a"; + private final String PERMISSION_DESCRIPTION = "Permission A description"; + private final PermissionId PERMISSION_ID = PermissionId.from(TENANT_ID, PERMISSION_NAME); + + private World world; + private MockDispatcher dispatcher; + + @BeforeEach + @SuppressWarnings({"unchecked", "rawtypes"}) + public void setUp() { + world = World.startWithDefaults("test-es"); + + dispatcher = new MockDispatcher(); + + EntryAdapterProvider entryAdapterProvider = EntryAdapterProvider.instance(world); + + entryAdapterProvider.registerAdapter(PermissionProvisioned.class, new PermissionProvisionedAdapter()); + entryAdapterProvider.registerAdapter(PermissionConstraintEnforced.class, new PermissionConstraintEnforcedAdapter()); + entryAdapterProvider.registerAdapter(PermissionConstraintReplacementEnforced.class, new PermissionConstraintReplacementEnforcedAdapter()); + entryAdapterProvider.registerAdapter(PermissionConstraintForgotten.class, new PermissionConstraintForgottenAdapter()); + entryAdapterProvider.registerAdapter(PermissionDescriptionChanged.class, new PermissionDescriptionChangedAdapter()); + + final Journal journal = world.actorFor(Journal.class, InMemoryJournalActor.class, Collections.singletonList(dispatcher)); + + new SourcedTypeRegistry(world).register(new Info(journal, PermissionEntity.class, PermissionEntity.class.getSimpleName())); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void permissionIsProvisionedForTheTenant() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Completes outcome = permissionOf(PERMISSION_ID) + .provisionPermission(PERMISSION_NAME, PERMISSION_DESCRIPTION); + + assertCompletes(outcome, state -> { + assertEquals(PERMISSION_NAME, state.name); + assertEquals(PERMISSION_DESCRIPTION, state.description); + assertEquals(PERMISSION_ID, state.id); + assertEventDispatched(dispatcherAccess, 1, PermissionProvisioned.class); + }); + } + + @Test + public void permissionConstraintIsEnforced() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Constraint constraint = Constraint.from(Constraint.Type.String, "constraint-name", "A", "constraint-description"); + + final Completes outcome = givenPermissionIsProvisioned(PERMISSION_ID) + .andThenTo(p -> permissionOf(PERMISSION_ID).enforce(constraint)); + + assertCompletes(outcome, state -> { + assertEquals(PERMISSION_ID, state.id); + assertContainsConstraint(constraint, state); + assertEventDispatched(dispatcherAccess, 2, PermissionConstraintEnforced.class); + }); + } + + @Test + public void permissionConstraintReplacementIsEnforced() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(3); + final Constraint constraintA = Constraint.from(Constraint.Type.String, "constraint-A", "A", "constraint-description"); + final Constraint replacementConstraint = Constraint.from(Constraint.Type.String, "constraint-B", "updated-permission-constraints-value", "updated-permission-constraints-description"); + + final Completes outcome = givenPermissionIsProvisioned(PERMISSION_ID) + .andThenTo(p -> permissionOf(PERMISSION_ID).enforce(constraintA)) + .andThenTo(p -> permissionOf(PERMISSION_ID).enforceReplacement("constraint-A", replacementConstraint)); + + assertCompletes(outcome, state -> { + assertEquals(1, state.constraints.size()); + assertNotContainsConstraint(constraintA, state); + assertContainsConstraint(replacementConstraint, state); + assertEventDispatched(dispatcherAccess, 3, PermissionConstraintReplacementEnforced.class); + }); + } + + @Test + public void permissionConstraintIsForgotten() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Constraint constraintA = Constraint.from(Constraint.Type.String, "constraint-A", "A", "constraint-description"); + final Constraint constraintB = Constraint.from(Constraint.Type.String, "constraint-B", "B", "constraint-description"); + + final Completes outcome = givenPermissionIsProvisioned(PERMISSION_ID) + .andThenTo(p -> permissionOf(PERMISSION_ID).enforce(constraintA)) + .andThenTo(p -> permissionOf(PERMISSION_ID).enforce(constraintB)) + .andThenTo(p -> permissionOf(PERMISSION_ID).forget("constraint-A")); + + assertCompletes(outcome, state -> { + assertEquals(1, state.constraints.size()); + assertNotContainsConstraint(constraintA, state); + assertContainsConstraint(constraintB, state); + assertEventDispatched(dispatcherAccess, 4, PermissionConstraintForgotten.class); + }); + } + + @Test + public void changeDescription() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenPermissionIsProvisioned(PERMISSION_ID) + .andThenTo(p -> permissionOf(PERMISSION_ID).changeDescription("updated-permission-description")); + + assertCompletes(outcome, state -> { + assertEquals(PERMISSION_NAME, state.name); + assertEquals("updated-permission-description", state.description); + assertEquals(PERMISSION_ID, state.id); + assertEventDispatched(dispatcherAccess, 2, PermissionDescriptionChanged.class); + }); + } + + private Completes givenPermissionIsProvisioned(final PermissionId permissionId) { + return permissionOf(permissionId).provisionPermission(PERMISSION_NAME, PERMISSION_DESCRIPTION); + } + + private Permission permissionOf(final PermissionId permissionId) { + return world.actorFor(Permission.class, PermissionEntity.class, permissionId); + } + + private void assertContainsConstraint(final Constraint constraint, final PermissionState state) { + assertEquals(new HashSet<>(Collections.singletonList(constraint)), state.constraints); + } + + private void assertNotContainsConstraint(final Constraint constraint, final PermissionState state) { + state.constraints.forEach(c -> assertNotEquals(constraint, c)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/permission/PermissionIdTest.java b/src/test/java/io/vlingo/xoom/auth/model/permission/PermissionIdTest.java new file mode 100644 index 00000000..e60c05de --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/permission/PermissionIdTest.java @@ -0,0 +1,27 @@ +package io.vlingo.xoom.auth.model.permission; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class PermissionIdTest { + @Test + public void itIsComposedOfTenantIdAndPermissionId() { + final PermissionId permissionId = PermissionId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "permission-a"); + + assertEquals("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be", permissionId.tenantId.id); + assertEquals("permission-a", permissionId.permissionName); + assertEquals("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be:permission-a", permissionId.idString()); + } + + @Test + public void twoPermissionIdsAreTheSameIfTenantIdAndPermissionIdAreTheSame() { + final PermissionId permissionId = PermissionId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "permission-a"); + + assertEquals(permissionId, PermissionId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "permission-a")); + assertNotEquals(permissionId, PermissionId.from(TenantId.from("97d57df1-1b38-4583-bd3c-6b4731e7a605"), "permission-a")); + assertNotEquals(permissionId, PermissionId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "permission-b")); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/role/RoleEntityTest.java b/src/test/java/io/vlingo/xoom/auth/model/role/RoleEntityTest.java new file mode 100644 index 00000000..59e3eb44 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/role/RoleEntityTest.java @@ -0,0 +1,204 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.auth.infrastructure.persistence.*; +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.permission.PermissionId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry.Info; +import io.vlingo.xoom.symbio.EntryAdapterProvider; +import io.vlingo.xoom.symbio.store.journal.Journal; +import io.vlingo.xoom.symbio.store.journal.inmemory.InMemoryJournalActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertEventDispatched; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RoleEntityTest { + + private final TenantId TENANT_ID = TenantId.from("f3e1f662-d7b4-4f9e-aa6c-6d16d4c70437"); + private final String ROLE_NAME = "role-a"; + private final String ROLE_DESCRIPTION = "Role A"; + private final RoleId ROLE_ID = RoleId.from(TENANT_ID, ROLE_NAME); + + private World world; + private MockDispatcher dispatcher; + + @BeforeEach + @SuppressWarnings({"unchecked", "rawtypes"}) + public void setUp() { + world = World.startWithDefaults("test-es"); + + dispatcher = new MockDispatcher(); + + final EntryAdapterProvider entryAdapterProvider = EntryAdapterProvider.instance(world); + entryAdapterProvider.registerAdapter(RoleProvisioned.class, new RoleProvisionedAdapter()); + entryAdapterProvider.registerAdapter(RoleDescriptionChanged.class, new RoleDescriptionChangedAdapter()); + entryAdapterProvider.registerAdapter(GroupAssignedToRole.class, new GroupAssignedToRoleAdapter()); + entryAdapterProvider.registerAdapter(GroupUnassignedFromRole.class, new GroupUnassignedFromRoleAdapter()); + entryAdapterProvider.registerAdapter(UserAssignedToRole.class, new UserAssignedToRoleAdapter()); + entryAdapterProvider.registerAdapter(UserUnassignedFromRole.class, new UserUnassignedFromRoleAdapter()); + entryAdapterProvider.registerAdapter(RolePermissionAttached.class, new RolePermissionAttachedAdapter()); + entryAdapterProvider.registerAdapter(RolePermissionDetached.class, new RolePermissionDetachedAdapter()); + + final Journal journal = world.actorFor(Journal.class, InMemoryJournalActor.class, Collections.singletonList(dispatcher)); + + new SourcedTypeRegistry(world).register(new Info(journal, RoleEntity.class, RoleEntity.class.getSimpleName())); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void roleIsProvisionedForTheTenant() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Completes outcome = roleOf(ROLE_ID).provisionRole(ROLE_NAME, ROLE_DESCRIPTION); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals(ROLE_NAME, state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 1, RoleProvisioned.class); + }); + } + + @Test + public void roleDescriptionIsChanged() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).changeDescription("updated-role-description")); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals(ROLE_NAME, state.name); + assertEquals("updated-role-description", state.description); + assertEventDispatched(dispatcherAccess, 2, RoleDescriptionChanged.class); + }); + } + + @Test + @Disabled("Test not implemented") + public void groupIsAssignedToRole() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final GroupId groupId = GroupId.from(TENANT_ID, "group-a"); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).assignGroup(groupId)); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals("updated-role-name", state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 2, GroupAssignedToRole.class); + }); + } + + @Test + @Disabled("Test not implemented") + public void groupIsUnassignedFromRole() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final GroupId groupId = GroupId.from(TENANT_ID, "group-a"); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).assignGroup(groupId)) + .andThenTo(r -> roleOf(ROLE_ID).unassignGroup(groupId)); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals("updated-role-name", state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 2, GroupUnassignedFromRole.class); + }); + } + + @Test + @Disabled("Test not implemented") + public void userIsAssignedToRole() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final UserId userId = UserId.from(TENANT_ID, "bobby"); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).assignUser(userId)); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals("updated-role-name", state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 2, UserAssignedToRole.class); + }); + } + + @Test + @Disabled("Test not implemented") + public void userIsUnassignedFromRole() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final UserId userId = UserId.from(TENANT_ID, "bobby"); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).assignUser(userId)) + .andThenTo(r -> roleOf(ROLE_ID).unassignUser(userId)); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals("updated-role-name", state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 2, UserUnassignedFromRole.class); + }); + } + + @Test + @Disabled("Test not implemented") + public void permissionIsAttachedToRole() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final PermissionId permissionId = PermissionId.from(TENANT_ID, "permission-a"); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).attach(permissionId)); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals("updated-role-name", state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 2, RolePermissionAttached.class); + }); + } + + @Test + @Disabled("Test not implemented") + public void permissionIsDetachedFromRole() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final PermissionId permissionId = PermissionId.from(TENANT_ID, "permission-a"); + + final Completes outcome = givenRoleExists(ROLE_ID) + .andThenTo(r -> roleOf(ROLE_ID).attach(permissionId)) + .andThenTo(state -> roleOf(ROLE_ID).detach(permissionId)); + + assertCompletes(outcome, state -> { + assertEquals(ROLE_ID, state.roleId); + assertEquals("updated-role-name", state.name); + assertEquals(ROLE_DESCRIPTION, state.description); + assertEventDispatched(dispatcherAccess, 2, RolePermissionDetached.class); + }); + } + + private Completes givenRoleExists(RoleId roleId) { + return roleOf(roleId).provisionRole(ROLE_NAME, ROLE_DESCRIPTION); + } + + private Role roleOf(RoleId roleId) { + return world.actorFor(Role.class, RoleEntity.class, roleId); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/role/RoleIdTest.java b/src/test/java/io/vlingo/xoom/auth/model/role/RoleIdTest.java new file mode 100644 index 00000000..552f918c --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/role/RoleIdTest.java @@ -0,0 +1,27 @@ +package io.vlingo.xoom.auth.model.role; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class RoleIdTest { + @Test + public void itIsComposedOfTenantIdAndRoleId() { + final RoleId roleId = RoleId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "role-a"); + + assertEquals("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be", roleId.tenantId.id); + assertEquals("role-a", roleId.roleName); + assertEquals("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be:role-a", roleId.idString()); + } + + @Test + public void twoRoleIdsAreTheSameIfTenantIdAndRoleIdAreTheSame() { + final RoleId roleId = RoleId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "role-a"); + + assertEquals(roleId, RoleId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "role-a")); + assertNotEquals(roleId, RoleId.from(TenantId.from("97d57df1-1b38-4583-bd3c-6b4731e7a605"), "role-a")); + assertNotEquals(roleId, RoleId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "role-b")); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/tenant/TenantEntityTest.java b/src/test/java/io/vlingo/xoom/auth/model/tenant/TenantEntityTest.java new file mode 100644 index 00000000..b3e14ceb --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/tenant/TenantEntityTest.java @@ -0,0 +1,132 @@ +package io.vlingo.xoom.auth.model.tenant; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.auth.infrastructure.persistence.*; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry.Info; +import io.vlingo.xoom.symbio.EntryAdapterProvider; +import io.vlingo.xoom.symbio.store.journal.Journal; +import io.vlingo.xoom.symbio.store.journal.inmemory.InMemoryJournalActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertEventDispatched; +import static org.junit.jupiter.api.Assertions.*; + +public class TenantEntityTest { + + private static final TenantId TENANT_ID = TenantId.from("05e66197-b1c8-4fe4-bf75-25855ec06fa5"); + private static final String TENANT_NAME = "tenant-name"; + private static final String TENANT_DESCRIPTION = "tenant-description"; + + private World world; + private MockDispatcher dispatcher; + + @BeforeEach + @SuppressWarnings({"unchecked", "rawtypes"}) + public void setUp(){ + world = World.startWithDefaults("test-es"); + + dispatcher = new MockDispatcher(); + + final EntryAdapterProvider entryAdapterProvider = EntryAdapterProvider.instance(world); + entryAdapterProvider.registerAdapter(TenantActivated.class, new TenantActivatedAdapter()); + entryAdapterProvider.registerAdapter(TenantDeactivated.class, new TenantDeactivatedAdapter()); + entryAdapterProvider.registerAdapter(TenantDescriptionChanged.class, new TenantDescriptionChangedAdapter()); + entryAdapterProvider.registerAdapter(TenantNameChanged.class, new TenantNameChangedAdapter()); + entryAdapterProvider.registerAdapter(TenantSubscribed.class, new TenantSubscribedAdapter()); + + final Journal journal = world.actorFor(Journal.class, InMemoryJournalActor.class, Collections.singletonList(dispatcher)); + + new SourcedTypeRegistry(world).register(new Info(journal, TenantEntity.class, TenantEntity.class.getSimpleName())); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void tenantSubscribesWithNameAndDescription() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Completes outcome = tenantOf(TENANT_ID) + .subscribeFor(TENANT_NAME, TENANT_DESCRIPTION, true); + + assertCompletes(outcome, state -> { + assertEquals(TENANT_NAME, state.name); + assertEquals(TENANT_DESCRIPTION, state.description); + assertTrue(state.active); + assertEventDispatched(dispatcherAccess, 1, TenantSubscribed.class); + }); + } + + @Test + public void tenantIsActivated() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenInactiveTenant(TENANT_ID) + .andThenTo(t -> tenantOf(TENANT_ID).activate()); + + assertCompletes(outcome, state -> { + assertTrue(state.active); + assertEventDispatched(dispatcherAccess, 2, TenantActivated.class); + }); + } + + @Test + public void tenantIsDeactivated() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenActiveTenant(TENANT_ID) + .andThenTo(t -> tenantOf(TENANT_ID).deactivate()); + + assertCompletes(outcome, state -> { + assertFalse(state.active); + assertEventDispatched(dispatcherAccess, 2, TenantDeactivated.class); + }); + } + + @Test + public void tenantNameIsChanged() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenActiveTenant(TENANT_ID) + .andThenTo(t -> tenantOf(TENANT_ID).changeName("updated-tenant-name")); + + assertCompletes(outcome, state -> { + assertEquals("updated-tenant-name", state.name); + assertEventDispatched(dispatcherAccess, 2, TenantNameChanged.class); + }); + } + + @Test + public void tenantDescriptionIsChanged() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenActiveTenant(TENANT_ID) + .andThenTo(t -> tenantOf(TENANT_ID).changeDescription("updated-tenant-description")); + + assertCompletes(outcome, state -> { + assertEquals("updated-tenant-description", state.description); + assertEventDispatched(dispatcherAccess, 2, TenantDescriptionChanged.class); + }); + } + + private Completes givenActiveTenant(final TenantId tenantId) { + return tenantOf(tenantId).subscribeFor(TENANT_NAME, TENANT_DESCRIPTION, true); + } + + private Completes givenInactiveTenant(final TenantId tenantId) { + return tenantOf(tenantId).subscribeFor(TENANT_NAME, TENANT_DESCRIPTION, false); + } + + private Tenant tenantOf(final TenantId tenantId) { + return world.actorFor(Tenant.class, TenantEntity.class, tenantId); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/tenant/TenantIdTest.java b/src/test/java/io/vlingo/xoom/auth/model/tenant/TenantIdTest.java new file mode 100644 index 00000000..acf1a496 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/tenant/TenantIdTest.java @@ -0,0 +1,30 @@ +package io.vlingo.xoom.auth.model.tenant; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TenantIdTest { + @Test + public void itWrapsTenantId() { + final TenantId tenantId = TenantId.from("c9ca8a96-eccd-416c-aba5-8c36cbaec717"); + + assertEquals("c9ca8a96-eccd-416c-aba5-8c36cbaec717", tenantId.id); + assertEquals("c9ca8a96-eccd-416c-aba5-8c36cbaec717", tenantId.idString()); + } + + @Test + public void twoTenantIdsAreTheSameIfItsStringIdsAreEqual() { + final TenantId tenantId = TenantId.from("c9ca8a96-eccd-416c-aba5-8c36cbaec717"); + + assertEquals(tenantId, TenantId.from("c9ca8a96-eccd-416c-aba5-8c36cbaec717")); + assertNotEquals(tenantId, TenantId.from("97d57df1-1b38-4583-bd3c-6b4731e7a605")); + } + + @Test + public void itGeneratesNewId() { + final TenantId tenantId = TenantId.unique(); + + assertNotNull(tenantId.id); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/user/UserEntityTest.java b/src/test/java/io/vlingo/xoom/auth/model/user/UserEntityTest.java new file mode 100644 index 00000000..cc21e545 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/user/UserEntityTest.java @@ -0,0 +1,190 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.actors.World; +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.auth.infrastructure.persistence.*; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.value.Credential; +import io.vlingo.xoom.auth.model.value.PersonName; +import io.vlingo.xoom.auth.model.value.Profile; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry; +import io.vlingo.xoom.lattice.model.sourcing.SourcedTypeRegistry.Info; +import io.vlingo.xoom.symbio.EntryAdapterProvider; +import io.vlingo.xoom.symbio.store.journal.Journal; +import io.vlingo.xoom.symbio.store.journal.inmemory.InMemoryJournalActor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import static io.vlingo.xoom.auth.test.Assertions.assertCompletes; +import static io.vlingo.xoom.auth.test.Assertions.assertEventDispatched; +import static org.junit.jupiter.api.Assertions.*; + +public class UserEntityTest { + + private final TenantId TENANT_ID = TenantId.from("8b727090-652a-4b02-90fc-2e890d09a1c5"); + private final String USER_USERNAME = "bob"; + private final Profile USER_PROFILE = Profile.from("bob@example.com", PersonName.from("Bob", "Smith", "Cecil"), "07926123123"); + private final Credential USER_CREDENTIAL = Credential.xoomCredentialFrom("user-credential-authority", "user-credential-id", "user-credential-secret"); + private final Set USER_CREDENTIALS = Collections.singleton(USER_CREDENTIAL); + private final UserId USER_ID = UserId.from(TENANT_ID, USER_USERNAME); + + private World world; + private MockDispatcher dispatcher; + + @BeforeEach + @SuppressWarnings({"unchecked", "rawtypes"}) + public void setUp(){ + world = World.startWithDefaults("test-es"); + + dispatcher = new MockDispatcher(); + + final EntryAdapterProvider entryAdapterProvider = EntryAdapterProvider.instance(world); + entryAdapterProvider.registerAdapter(UserRegistered.class, new UserRegisteredAdapter()); + entryAdapterProvider.registerAdapter(UserActivated.class, new UserActivatedAdapter()); + entryAdapterProvider.registerAdapter(UserDeactivated.class, new UserDeactivatedAdapter()); + entryAdapterProvider.registerAdapter(UserCredentialAdded.class, new UserCredentialAddedAdapter()); + entryAdapterProvider.registerAdapter(UserCredentialRemoved.class, new UserCredentialRemovedAdapter()); + entryAdapterProvider.registerAdapter(UserCredentialReplaced.class, new UserCredentialReplacedAdapter()); + entryAdapterProvider.registerAdapter(UserProfileReplaced.class, new UserProfileReplacedAdapter()); + + final Journal journal = world.actorFor(Journal.class, InMemoryJournalActor.class, Collections.singletonList(dispatcher)); + + new SourcedTypeRegistry(world).register(new Info(journal, UserEntity.class, UserEntity.class.getSimpleName())); + } + + @AfterEach + public void tearDown() { + world.terminate(); + } + + @Test + public void userIsRegistered() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Completes outcome = userOf(USER_ID) + .registerUser(USER_USERNAME, USER_PROFILE, USER_CREDENTIALS, true); + + assertCompletes(outcome, state -> { + assertEquals(USER_ID, state.userId); + assertEquals(USER_USERNAME, state.username); + assertEquals(true, state.active); + assertEquals(USER_PROFILE, state.profile); + assertEquals(USER_CREDENTIALS, state.credentials); + assertEventDispatched(dispatcherAccess, 1, UserRegistered.class); + }); + } + + @Test + public void userIsActivated() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenInactiveUser(USER_ID) + .andThenTo(u -> userOf(USER_ID).activate()); + + assertCompletes(outcome, state -> { + assertTrue(state.active); + assertEventDispatched(dispatcherAccess, 2, UserActivated.class); + }); + } + + @Test + public void userIsDeactivated() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenActiveUser(USER_ID) + .andThenTo(u -> userOf(USER_ID).deactivate()); + + assertCompletes(outcome, state -> { + assertFalse(state.active); + assertEventDispatched(dispatcherAccess, 2, UserDeactivated.class); + }); + } + + @Test + public void credentialsAreAddedToRegisteredUser() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Credential newCredential = Credential.xoomCredentialFrom("updated-user-credentials-authority", "updated-1", "updated-user-credentials-secret"); + + final Completes outcome = givenActiveUser(USER_ID) + .andThenTo(u -> userOf(USER_ID).addCredential(newCredential)); + + assertCompletes(outcome, state -> { + assertEquals(USER_ID, state.userId); + assertContainsCredential(USER_CREDENTIAL, state); + assertContainsCredential(newCredential, state); + assertEventDispatched(dispatcherAccess, 2, UserCredentialAdded.class); + }); + } + + @Test + public void credentialsAreRemovedFromRegisteredUser() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + + final Completes outcome = givenActiveUser(USER_ID) + .andThenTo(u -> userOf(USER_ID).removeCredential(USER_CREDENTIAL.authority)); + + assertCompletes(outcome, state -> { + assertEquals(USER_ID, state.userId); + assertNotContainsCredential(USER_CREDENTIAL, state); + assertEquals(0, state.credentials.size()); + assertEventDispatched(dispatcherAccess, 2, UserCredentialRemoved.class); + }); + } + + @Test + public void credentialsAreReplaced() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Credential newCredential = Credential.xoomCredentialFrom("updated-authority", "updated-user-credentials-id", "updated-user-credentials-secret"); + + final Completes outcome = givenActiveUser(USER_ID) + .andThenTo(u -> userOf(USER_ID).replaceCredential(USER_CREDENTIAL.authority, newCredential)); + + assertCompletes(outcome, state -> { + assertEquals(USER_ID, state.userId); + assertContainsCredential(newCredential, state); + assertNotContainsCredential(USER_CREDENTIAL, state); + assertEventDispatched(dispatcherAccess, 2, UserCredentialReplaced.class); + }); + } + + @Test + public void profileIsReplaced() { + final AccessSafely dispatcherAccess = dispatcher.afterCompleting(1); + final Profile updatedProfile = Profile.from("updated-user-profile-emailAddress", PersonName.from("updated-user-profile-name-given", "updated-user-profile-name-family", "updated-user-profile-name-second"), "updated-user-profile-phone"); + + final Completes outcome = givenActiveUser(USER_ID) + .andThenTo(u -> userOf(USER_ID).replaceProfile(updatedProfile)); + + assertCompletes(outcome, state -> { + assertEquals(updatedProfile, state.profile); + assertEventDispatched(dispatcherAccess, 2, UserProfileReplaced.class); + }); + } + + private User userOf(final UserId userId) { + return world.actorFor(User.class, UserEntity.class, userId); + } + + private Completes givenInactiveUser(final UserId userId) { + return userOf(userId).registerUser(USER_USERNAME, USER_PROFILE, USER_CREDENTIALS, false); + } + + private Completes givenActiveUser(final UserId userId) { + return userOf(userId).registerUser(USER_USERNAME, USER_PROFILE, USER_CREDENTIALS, true); + } + + private void assertContainsCredential(final Credential credential, final UserState state) { + Optional foundCredential = state.credentials.stream().filter(c -> c.equals(credential)).findFirst(); + assertTrue(foundCredential.isPresent(), String.format("Credential not found %s", credential)); + } + + private void assertNotContainsCredential(final Credential credential, final UserState state) { + Optional foundCredential = state.credentials.stream().filter(c -> c.equals(credential)).findFirst(); + assertFalse(foundCredential.isPresent(), String.format("Credential found %s", credential)); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/user/UserIdTest.java b/src/test/java/io/vlingo/xoom/auth/model/user/UserIdTest.java new file mode 100644 index 00000000..d07f397d --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/user/UserIdTest.java @@ -0,0 +1,34 @@ +package io.vlingo.xoom.auth.model.user; + +import io.vlingo.xoom.auth.model.tenant.TenantId; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class UserIdTest { + @Test + public void itIsComposedOfTenantIdAndUsername() { + final UserId userId = UserId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "alice"); + + assertEquals("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be", userId.tenantId.id); + assertEquals("alice", userId.username); + assertEquals("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be:alice", userId.idString()); + } + + @Test + public void twoUserIdsAreTheSameIfTenantIdAndUsernameAreTheSame() { + final UserId userId = UserId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "alice"); + + assertEquals(userId, UserId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "alice")); + assertNotEquals(userId, UserId.from(TenantId.from("97d57df1-1b38-4583-bd3c-6b4731e7a605"), "alice")); + assertNotEquals(userId, UserId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "bob")); + } + + @Test + public void itIsCreatedFromString() { + final UserId userId = UserId.from(TenantId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be"), "alice"); + + assertEquals(userId, UserId.from("c60317e9-cf4f-408a-9fd1-8e0f4e69d2be:alice")); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/model/value/EncodedMemberTest.java b/src/test/java/io/vlingo/xoom/auth/model/value/EncodedMemberTest.java new file mode 100644 index 00000000..5f215e75 --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/model/value/EncodedMemberTest.java @@ -0,0 +1,39 @@ +package io.vlingo.xoom.auth.model.value; + +import io.vlingo.xoom.auth.model.group.GroupId; +import io.vlingo.xoom.auth.model.tenant.TenantId; +import io.vlingo.xoom.auth.model.user.UserId; +import io.vlingo.xoom.auth.model.value.EncodedMember.GroupMember; +import io.vlingo.xoom.auth.model.value.EncodedMember.UserMember; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class EncodedMemberTest { + + @Test + public void itCreatesGroupMembers() { + final GroupId groupId = GroupId.from(TenantId.unique(), "group-a"); + final GroupMember member = EncodedMember.group(groupId); + + assertEquals(groupId.idString(), member.id); + } + + @Test + public void twoMembersAreTheSameIfGivenTheSameId() { + final GroupId groupId = GroupId.from(TenantId.unique(), "group-a"); + final GroupMember member = EncodedMember.group(groupId); + + assertEquals(member, EncodedMember.group(groupId)); + assertNotEquals(member, EncodedMember.group(GroupId.from(TenantId.unique(), "group-b"))); + } + + @Test + public void itCreatesUserMembers() { + final UserId userId = UserId.from(TenantId.unique(), "bobby"); + final UserMember member = EncodedMember.user(userId); + + assertEquals(userId.idString(), member.id); + } +} diff --git a/src/test/java/io/vlingo/xoom/auth/test/Assertions.java b/src/test/java/io/vlingo/xoom/auth/test/Assertions.java new file mode 100644 index 00000000..191120ce --- /dev/null +++ b/src/test/java/io/vlingo/xoom/auth/test/Assertions.java @@ -0,0 +1,72 @@ +package io.vlingo.xoom.auth.test; + +import io.vlingo.xoom.actors.testkit.AccessSafely; +import io.vlingo.xoom.common.Completes; +import io.vlingo.xoom.symbio.BaseEntry; + +import java.util.Collection; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; + +public class Assertions { + + + /** + * Asserts if the item is included in the collection. + * + * @param item the item to be searched for + * @param items the collection + * @param type of the item + */ + public static void assertContains(final I item, final Collection items) { + assertTrue(items.stream().filter(i -> i.equals(item)).findFirst().isPresent(), String.format("Item %s not found in %s", item, items)); + } + + /** + * Asserts if the item is NOT included in the collection. + * + * @param item the unexpected item + * @param items the collection + * @param type of the item + */ + public static void assertNotContains(final I item, final Collection items) { + assertFalse(items.stream().filter(i -> i.equals(item)).findFirst().isPresent(), String.format("Unexpected item %s found in %s", item, items)); + } + + /** + * Asserts if all expected items are included in the collection + * + * @param expectedItems items to be searched for + * @param items the collection + * @param type of items + */ + public static void assertContainsAll(final Collection expectedItems, Collection items) { + expectedItems.forEach(item -> assertContains(item, items)); + } + + /** + * Asserts the number of dispatched events and the type of the last dispatched event. + * + * @param dispatcherAccess Access to dispatcher's state (entriesCount) + * @param sequence the event sequence to assert on + * @param expectedEvent the expected event type + */ + public static void assertEventDispatched(final AccessSafely dispatcherAccess, final int sequence, final Class expectedEvent) { + assertEquals(sequence, (int) dispatcherAccess.readFrom("entriesCount"), String.format("Expected at least %d events", sequence)); + assertEquals(expectedEvent.getName(), ((BaseEntry) dispatcherAccess.readFrom("appendedAt", sequence - 1)).typeName(), String.format("Expected the %d event in the sequence to be %s", sequence, expectedEvent.getName())); + } + + /** + * Makes assertions on an outcome of an asynchronous operation. + * + * @param completes the eventual completion of an asynchronous operation + * @param assertions the consumer of the completed operation that will be called to make assertions + * @param the type of the outcome + */ + public static void assertCompletes(final Completes completes, final Consumer assertions) { + final T outcome = completes.await(3000); + assertNotEquals(null, outcome); + assertions.accept(outcome); + } +} diff --git a/xoom-auth-designer-model.json b/xoom-auth-designer-model.json new file mode 100644 index 00000000..2c1d1f9a --- /dev/null +++ b/xoom-auth-designer-model.json @@ -0,0 +1,1304 @@ +{ + "context": { + "groupId": "io.vlingo.xoom", + "artifactId": "xoom-auth", + "artifactVersion": "1.8.6", + "packageName": "io.vlingo.xoom.auth" + }, + "model": { + "persistenceSettings": { + "storageType": "JOURNAL", + "useCQRS": true, + "projections": "EVENT_BASED", + "database": "IN_MEMORY", + "commandModelDatabase": "IN_MEMORY", + "queryModelDatabase": "IN_MEMORY" + }, + "aggregateSettings": [ + { + "api": { + "rootPath": "/tenants", + "routes": [ + { + "path": "/{tenantId}/roles", + "httpMethod": "POST", + "aggregateMethod": "provisionRole", + "requireEntityLoad": false + }, + { + "path": "/{tenantId}/roles/{roleName}/description", + "httpMethod": "PATCH", + "aggregateMethod": "changeDescription", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/roles/{roleName}/groups", + "httpMethod": "PUT", + "aggregateMethod": "assignGroup", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/roles/{roleName}/groups/{groupName}", + "httpMethod": "DELETE", + "aggregateMethod": "unassignGroup", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/roles/{roleName}/users", + "httpMethod": "PUT", + "aggregateMethod": "assignUser", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/roles/{roleName}/users/{username}", + "httpMethod": "DELETE", + "aggregateMethod": "unassignUser", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/roles/{roleName}/permissions", + "httpMethod": "PUT", + "aggregateMethod": "attach", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/roles/{roleName}/permissions/{permissionName}", + "httpMethod": "DELETE", + "aggregateMethod": "detach", + "requireEntityLoad": true + } + ] + }, + "aggregateName": "Role", + "stateFields": [ + { + "name": "id", + "type": "String" + }, + { + "name": "tenantId", + "type": "String", + "collectionType": "" + }, + { + "name": "name", + "type": "String", + "collectionType": "" + }, + { + "name": "description", + "type": "String", + "collectionType": "" + } + ], + "methods": [ + { + "name": "provisionRole", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + }, + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + } + ], + "useFactory": true, + "event": "RoleProvisioned" + }, + { + "name": "changeDescription", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + }, + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "RoleDescriptionChanged" + }, + { + "name": "assignGroup", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "GroupAssignedToRole" + }, + { + "name": "unassignGroup", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "GroupUnassignedFromRole" + }, + { + "name": "assignUser", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserAssignedToRole" + }, + { + "name": "unassignUser", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserUnassignedFromRole" + }, + { + "name": "attach", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "RolePermissionAttached" + }, + { + "name": "detach", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "RolePermissionDetached" + } + ], + "events": [ + { + "name": "RoleProvisioned", + "fields": [ + "id", + "tenantId", + "name", + "description" + ] + }, + { + "name": "RoleDescriptionChanged", + "fields": [ + "id", + "tenantId", + "name", + "description" + ] + }, + { + "name": "GroupAssignedToRole", + "fields": [ + "id", + "tenantId", + "name" + ] + }, + { + "name": "GroupUnassignedFromRole", + "fields": [ + "id", + "tenantId", + "name" + ] + }, + { + "name": "UserAssignedToRole", + "fields": [ + "id", + "tenantId", + "name" + ] + }, + { + "name": "UserUnassignedFromRole", + "fields": [ + "id", + "tenantId", + "name" + ] + }, + { + "name": "RolePermissionAttached", + "fields": [ + "id", + "tenantId", + "name" + ] + }, + { + "name": "RolePermissionDetached", + "fields": [ + "id", + "tenantId", + "name" + ] + } + ], + "consumerExchange": { + "exchangeName": "xoom-auth-topic", + "receivers": [] + }, + "producerExchange": { + "exchangeName": "xoom-auth-topic", + "schemaGroup": "vlingo:xoom:auth", + "outgoingEvents": [ + "RoleProvisioned", + "RoleDescriptionChanged", + "GroupAssignedToRole", + "GroupUnassignedFromRole", + "UserUnassignedFromRole", + "UserAssignedToRole", + "RolePermissionAttached", + "RolePermissionDetached" + ] + } + }, + { + "api": { + "rootPath": "/tenants", + "routes": [ + { + "path": "*", + "httpMethod": "POST", + "aggregateMethod": "subscribeFor", + "requireEntityLoad": false + }, + { + "path": "/{id}/activate", + "httpMethod": "PATCH", + "aggregateMethod": "activate", + "requireEntityLoad": true + }, + { + "path": "/{id}/deactivate", + "httpMethod": "PATCH", + "aggregateMethod": "deactivate", + "requireEntityLoad": true + }, + { + "path": "/{id}/description", + "httpMethod": "PATCH", + "aggregateMethod": "changeDescription", + "requireEntityLoad": true + }, + { + "path": "/{id}/name", + "httpMethod": "PATCH", + "aggregateMethod": "changeName", + "requireEntityLoad": true + } + ] + }, + "aggregateName": "Tenant", + "stateFields": [ + { + "name": "id", + "type": "String" + }, + { + "name": "name", + "type": "String", + "collectionType": "" + }, + { + "name": "description", + "type": "String", + "collectionType": "" + }, + { + "name": "active", + "type": "boolean", + "collectionType": "" + } + ], + "methods": [ + { + "name": "subscribeFor", + "parameters": [ + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + }, + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + }, + { + "stateField": "active", + "parameterName": "active", + "multiplicity": "" + } + ], + "useFactory": true, + "event": "TenantSubscribed" + }, + { + "name": "activate", + "parameters": [], + "useFactory": false, + "event": "TenantActivated" + }, + { + "name": "deactivate", + "parameters": [], + "useFactory": false, + "event": "TenantDeactivated" + }, + { + "name": "changeName", + "parameters": [ + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "TenantNameChanged" + }, + { + "name": "changeDescription", + "parameters": [ + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "TenantDescriptionChanged" + } + ], + "events": [ + { + "name": "TenantActivated", + "fields": [ + "id" + ] + }, + { + "name": "TenantDeactivated", + "fields": [ + "id" + ] + }, + { + "name": "TenantDescriptionChanged", + "fields": [ + "id", + "description" + ] + }, + { + "name": "TenantNameChanged", + "fields": [ + "id", + "name" + ] + }, + { + "name": "TenantSubscribed", + "fields": [ + "id", + "name", + "description", + "active" + ] + } + ], + "consumerExchange": { + "exchangeName": "xoom-auth-topic", + "receivers": [] + }, + "producerExchange": { + "exchangeName": "xoom-auth-topic", + "schemaGroup": "vlingo:xoom:auth", + "outgoingEvents": [ + "TenantActivated", + "TenantDeactivated", + "TenantDescriptionChanged", + "TenantNameChanged", + "TenantSubscribed" + ] + } + }, + { + "api": { + "rootPath": "/tenants", + "routes": [ + { + "path": "/{tenantId}/groups", + "httpMethod": "POST", + "aggregateMethod": "provisionGroup", + "requireEntityLoad": false + }, + { + "path": "/{tenantId}/groups/{groupName}/description", + "httpMethod": "PATCH", + "aggregateMethod": "changeDescription", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/groups/{groupName}/groups/{innerGroupName}", + "httpMethod": "PUT", + "aggregateMethod": "assignGroup", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/groups/{groupName}/groups/{innerGroupName}", + "httpMethod": "DELETE", + "aggregateMethod": "unassignGroup", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/groups/{groupName}/users/{username}", + "httpMethod": "PUT", + "aggregateMethod": "assignUser", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/groups/{groupName}/users/{username}", + "httpMethod": "DELETE", + "aggregateMethod": "unassignUser", + "requireEntityLoad": true + } + ] + }, + "aggregateName": "Group", + "stateFields": [ + { + "name": "id", + "type": "String" + }, + { + "name": "name", + "type": "String", + "collectionType": "" + }, + { + "name": "description", + "type": "String", + "collectionType": "" + }, + { + "name": "tenantId", + "type": "String", + "collectionType": "" + } + ], + "methods": [ + { + "name": "provisionGroup", + "parameters": [ + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + }, + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + }, + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": true, + "event": "GroupProvisioned" + }, + { + "name": "changeDescription", + "parameters": [ + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + }, + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "GroupDescriptionChanged" + }, + { + "name": "assignGroup", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "GroupAssignedToGroup" + }, + { + "name": "unassignGroup", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "GroupUnassignedFromGroup" + }, + { + "name": "assignUser", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserAssignedToGroup" + }, + { + "name": "unassignUser", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserUnassignedFromGroup" + } + ], + "events": [ + { + "name": "GroupProvisioned", + "fields": [ + "id", + "name", + "description", + "tenantId" + ] + }, + { + "name": "GroupDescriptionChanged", + "fields": [ + "id", + "description", + "tenantId" + ] + }, + { + "name": "GroupAssignedToGroup", + "fields": [ + "id", + "tenantId" + ] + }, + { + "name": "GroupUnassignedFromGroup", + "fields": [ + "id", + "tenantId" + ] + }, + { + "name": "UserAssignedToGroup", + "fields": [ + "id", + "tenantId" + ] + }, + { + "name": "UserUnassignedFromGroup", + "fields": [ + "id", + "tenantId" + ] + } + ], + "consumerExchange": { + "exchangeName": "xoom-auth-topic", + "receivers": [] + }, + "producerExchange": { + "exchangeName": "xoom-auth-topic", + "schemaGroup": "vlingo:xoom:auth", + "outgoingEvents": [ + "GroupProvisioned", + "GroupDescriptionChanged", + "GroupAssignedToGroup", + "GroupUnassignedFromGroup", + "UserAssignedToGroup", + "UserUnassignedFromGroup" + ] + } + }, + { + "api": { + "rootPath": "/tenants", + "routes": [ + { + "path": "/{tenantId}/users", + "httpMethod": "POST", + "aggregateMethod": "registerUser", + "requireEntityLoad": false + }, + { + "path": "/{tenantId}/users/{username}/activate", + "httpMethod": "PATCH", + "aggregateMethod": "activate", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/users/{username}/deactivate", + "httpMethod": "PATCH", + "aggregateMethod": "deactivate", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/users/{username}/credentials", + "httpMethod": "PUT", + "aggregateMethod": "addCredential", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/users/{username}/credentials/{authority}", + "httpMethod": "DELETE", + "aggregateMethod": "removeCredential", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/users/{username}/credentials/{authority}", + "httpMethod": "PATCH", + "aggregateMethod": "replaceCredential", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/users/{username}/profile", + "httpMethod": "PATCH", + "aggregateMethod": "replaceProfile", + "requireEntityLoad": true + } + ] + }, + "aggregateName": "User", + "stateFields": [ + { + "name": "id", + "type": "String" + }, + { + "name": "tenantId", + "type": "String", + "collectionType": "" + }, + { + "name": "username", + "type": "String", + "collectionType": "" + }, + { + "name": "active", + "type": "boolean", + "collectionType": "" + }, + { + "name": "credentials", + "type": "Credential", + "collectionType": "Set" + }, + { + "name": "profile", + "type": "Profile", + "collectionType": "" + } + ], + "methods": [ + { + "name": "registerUser", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "username", + "parameterName": "username", + "multiplicity": "" + }, + { + "stateField": "active", + "parameterName": "active", + "multiplicity": "" + }, + { + "stateField": "profile", + "parameterName": "profile", + "multiplicity": "" + } + ], + "useFactory": true, + "event": "UserRegistered" + }, + { + "name": "activate", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "username", + "parameterName": "username", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserActivated" + }, + { + "name": "deactivate", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "username", + "parameterName": "username", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserDeactivated" + }, + { + "name": "addCredential", + "parameters": [ + { + "stateField": "credentials", + "parameterName": "credential", + "multiplicity": "+" + } + ], + "useFactory": false, + "event": "UserCredentialAdded" + }, + { + "name": "removeCredential", + "parameters": [ + { + "stateField": "credentials", + "parameterName": "credential", + "multiplicity": "-" + } + ], + "useFactory": false, + "event": "UserCredentialRemoved" + }, + { + "name": "replaceCredential", + "parameters": [ + { + "stateField": "credentials", + "parameterName": "credential", + "multiplicity": "+" + } + ], + "useFactory": false, + "event": "UserCredentialReplaced" + }, + { + "name": "replaceProfile", + "parameters": [ + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + }, + { + "stateField": "username", + "parameterName": "username", + "multiplicity": "" + }, + { + "stateField": "profile", + "parameterName": "profile", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "UserProfileReplaced" + } + ], + "events": [ + { + "name": "UserRegistered", + "fields": [ + "id", + "tenantId", + "username", + "active", + "profile" + ] + }, + { + "name": "UserActivated", + "fields": [ + "id", + "tenantId", + "username" + ] + }, + { + "name": "UserDeactivated", + "fields": [ + "id", + "tenantId", + "username" + ] + }, + { + "name": "UserCredentialAdded", + "fields": [ + "id", + "credentials" + ] + }, + { + "name": "UserCredentialRemoved", + "fields": [ + "id", + "credentials" + ] + }, + { + "name": "UserCredentialReplaced", + "fields": [ + "id", + "credentials" + ] + }, + { + "name": "UserProfileReplaced", + "fields": [ + "id", + "tenantId", + "username", + "profile" + ] + } + ], + "consumerExchange": { + "exchangeName": "xoom-auth-topic", + "receivers": [] + }, + "producerExchange": { + "exchangeName": "xoom-auth-topic", + "schemaGroup": "vlingo:xoom:auth", + "outgoingEvents": [ + "UserRegistered", + "UserActivated", + "UserDeactivated", + "UserCredentialAdded", + "UserCredentialRemoved", + "UserCredentialReplaced", + "UserProfileReplaced" + ] + } + }, + { + "api": { + "rootPath": "/tenants", + "routes": [ + { + "path": "/{tenantId}/permissions", + "httpMethod": "POST", + "aggregateMethod": "provisionPermission", + "requireEntityLoad": false + }, + { + "path": "/{tenantId}/permissions/{permissionName}/constraints", + "httpMethod": "PATCH", + "aggregateMethod": "enforce", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/permissions/{permissionName}/constraints/{constraintName}", + "httpMethod": "PATCH", + "aggregateMethod": "enforceReplacement", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/permissions/{permissionName}/constraints/{constraintName}", + "httpMethod": "DELETE", + "aggregateMethod": "forget", + "requireEntityLoad": true + }, + { + "path": "/{tenantId}/permissions/{permissionName}/description", + "httpMethod": "PATCH", + "aggregateMethod": "changeDescription", + "requireEntityLoad": true + } + ] + }, + "aggregateName": "Permission", + "stateFields": [ + { + "name": "id", + "type": "String" + }, + { + "name": "constraints", + "type": "Constraint", + "collectionType": "Set" + }, + { + "name": "description", + "type": "String", + "collectionType": "" + }, + { + "name": "name", + "type": "String", + "collectionType": "" + }, + { + "name": "tenantId", + "type": "String", + "collectionType": "" + } + ], + "methods": [ + { + "name": "provisionPermission", + "parameters": [ + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + }, + { + "stateField": "name", + "parameterName": "name", + "multiplicity": "" + }, + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": true, + "event": "PermissionProvisioned" + }, + { + "name": "enforce", + "parameters": [ + { + "stateField": "constraints", + "parameterName": "constraint", + "multiplicity": "+" + } + ], + "useFactory": false, + "event": "PermissionConstraintEnforced" + }, + { + "name": "enforceReplacement", + "parameters": [ + { + "stateField": "constraints", + "parameterName": "constraint", + "multiplicity": "+" + } + ], + "useFactory": false, + "event": "PermissionConstraintReplacementEnforced" + }, + { + "name": "forget", + "parameters": [ + { + "stateField": "constraints", + "parameterName": "constraint", + "multiplicity": "+" + } + ], + "useFactory": false, + "event": "PermissionConstraintForgotten" + }, + { + "name": "changeDescription", + "parameters": [ + { + "stateField": "description", + "parameterName": "description", + "multiplicity": "" + }, + { + "stateField": "tenantId", + "parameterName": "tenantId", + "multiplicity": "" + } + ], + "useFactory": false, + "event": "PermissionDescriptionChanged" + } + ], + "events": [ + { + "name": "PermissionProvisioned", + "fields": [ + "id", + "description", + "name", + "tenantId" + ] + }, + { + "name": "PermissionConstraintEnforced", + "fields": [ + "id", + "constraints" + ] + }, + { + "name": "PermissionConstraintReplacementEnforced", + "fields": [ + "id", + "constraints" + ] + }, + { + "name": "PermissionConstraintForgotten", + "fields": [ + "id", + "constraints" + ] + }, + { + "name": "PermissionDescriptionChanged", + "fields": [ + "id", + "description", + "tenantId" + ] + } + ], + "consumerExchange": { + "exchangeName": "xoom-auth-topic", + "receivers": [] + }, + "producerExchange": { + "exchangeName": "xoom-auth-topic", + "schemaGroup": "vlingo:xoom:auth", + "outgoingEvents": [ + "PermissionProvisioned", + "PermissionConstraintEnforced", + "PermissionConstraintReplacementEnforced", + "PermissionConstraintForgotten", + "PermissionDescriptionChanged" + ] + } + } + ], + "valueObjectSettings": [ + { + "name": "Constraint", + "fields": [ + { + "name": "description", + "type": "String", + "collectionType": "" + }, + { + "name": "name", + "type": "String", + "collectionType": "" + }, + { + "name": "type", + "type": "String", + "collectionType": "" + }, + { + "name": "value", + "type": "String", + "collectionType": "" + } + ] + }, + { + "name": "Credential", + "fields": [ + { + "name": "authority", + "type": "String", + "collectionType": "" + }, + { + "name": "id", + "type": "String", + "collectionType": "" + }, + { + "name": "secret", + "type": "String", + "collectionType": "" + }, + { + "name": "type", + "type": "String", + "collectionType": "" + } + ] + }, + { + "name": "PersonName", + "fields": [ + { + "name": "given", + "type": "String", + "collectionType": "" + }, + { + "name": "family", + "type": "String", + "collectionType": "" + }, + { + "name": "second", + "type": "String", + "collectionType": "" + } + ] + }, + { + "name": "Profile", + "fields": [ + { + "name": "emailAddress", + "type": "String", + "collectionType": "" + }, + { + "name": "name", + "type": "PersonName", + "collectionType": "" + }, + { + "name": "phone", + "type": "String", + "collectionType": "" + } + ] + } + ] + }, + "deployment": { + "type": "NONE", + "dockerImage": "", + "kubernetesImage": "", + "kubernetesPod": "", + "clusterTotalNodes": 3, + "clusterPort": 38001, + "producerExchangePort": 37001, + "httpServerPort": 8080, + "pullSchemas": false + }, + "schemata": { + "host": "xoom-schemata", + "port": 9019 + }, + "projectDirectory": "/designer/VLINGO-XOOM/io.vlingo.xoom/xoom-auth12", + "useAnnotations": true, + "useAutoDispatch": false, + "generateUIWith": "ReactJS", + "generateUI": false +} \ No newline at end of file