src/main/java/org/ovirt/engine/extension/aaa/jdbc/binding/cli/command/GroupManageShowCommand.java
public class GroupManageShowCommand extends Command {

    @Override
    public String getName() {
        return "root-group-manage-show";
    }

    @Override
    public boolean entityNameExpected() {
        return true; // group name expected
    }

    @Override
    public void invoke(ExtMap context, Map<String, Object> args) {
        try {
            //Load Group fields into a map.
            ExtMap resultsMap = getGroup(context);
            if (resultsMap != null) {
                //Get group name, uuid and database-id (the latter is necessary for joins).
                String groupName = (String) resultsMap.get(Authz.GroupRecord.NAME);
                String groupUuid = (String) resultsMap.get(Authz.GroupRecord.ID);
                Integer groupDbId = (Integer) resultsMap.get(Schema.GroupKeys.DB_ID);

                //get users and groups associated with this group.
                DataSource dataSource = context.get(InvokeKeys.DATA_SOURCE, DataSource.class);
                List<String> groups = getMemberGroups(groupDbId, dataSource);
                List<String> users = getMemberUsers(groupDbId, dataSource);

                //display the results
                show(context, groupName, groupUuid, groups, users);
            }
        } catch (SQLException e) {
            context.put(ContextKeys.EXIT_STATUS, Cli.SQL_ERROR);
            addContextMessage(context, true, e.getMessage());
            context.<List<Throwable>>get(ContextKeys.THROWABLES).add(e);
        }
    }

    /**
     * Fetches the group from the database and fills its fields in a map.
     */
    private ExtMap getGroup(ExtMap context) {
        ExtMap resultsMap = null;
        Cli.getGroup(context);
        if (context.get(ContextKeys.SEARCH_RESULT) != null && !context.get(ContextKeys.SEARCH_RESULT, Collection.class).isEmpty()) {
            resultsMap = (ExtMap) (context.get(ContextKeys.SEARCH_RESULT, Collection.class).iterator().next());
        }
        return resultsMap;
    }


    /**
     * Gets users which are members of this group.
     */
    private List<String> getMemberUsers(Integer groupId, DataSource dataSource) throws SQLException {
        String sqlQuery = "select u.name as name from users u, user_groups ug where ug.in_group_id=" + groupId + " and u.id=ug.user_id";
        List<String> results = new Sql.Query(sqlQuery).asResults(dataSource, NameListResolver.instance);
        return results;
    }


    /**
     * Gets groups which are members of this group.
     */
    private List<String> getMemberGroups(Integer groupId, DataSource dataSource) throws SQLException {
        String sqlQuery = "select g.name as name from groups g, group_groups gg where gg.in_group_id=" + groupId + " and g.id=gg.group_id";
        List<String> results = new Sql.Query(sqlQuery).asResults(dataSource, NameListResolver.instance);
        return results;
    }


    /**
     * Displays the output of the command in the following format:
     *
     *   Group some_group(cedecfe3-846c-4428-acb6-e2a01ffb4951) members:
     *     user: user_1
     *     user: user_2
     *     group: group_1
     *     group: group_2
     *
     * If the group has no members, the display is:
     *
     *   Group some_group(cedecfe3-846c-4428-acb6-e2a01ffb4951) is empty.
     */
    private void show(ExtMap context, String groupName, String groupUuid, List<String> memberGroups, List<String> memberUsers) {
        StringBuilder builder = new StringBuilder();
        builder.append("Group: ").append(groupName).append("(").append(groupUuid).append(")");
        if (memberGroups.isEmpty() && memberUsers.isEmpty()) {
            builder.append(" has no members.");
        } else {
            builder.append(" members:\n");
            for (String user : memberUsers) {
                builder.append("  User: ").append(user).append("\n");
            }
            for (String group : memberGroups) {
                builder.append("  Group: ").append(group).append("\n");
            }
        }
        String output = builder.toString().trim();
        addContextMessage(context, false, output);
    }

    /**
     * A Resolver that knows how to handle such results:
     *    name=a,
     *    name=b,
     *    name-c
     *    .
     *    .
     *    .
     * (both queries - for users and for groups - return the results in this format).
     */
    private static class NameListResolver implements Sql.ResultsResolver<List<String>>{
        static NameListResolver instance = new NameListResolver();
        @Override
        public List<String> resolve(ResultSet resultSet, ExtMap context) throws SQLException {
            List<String> results = new LinkedList<>();
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                results.add(name);
            }
            return results;
        }
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/core/datasource/DataSourceProvider.java
public class DataSourceProvider {

    private static final String JNDI = "config.datasource.jndi";

    private static final String JDBC_DRIVER = "config.datasource.jdbcdriver";

    private static final String JDBC_URL = "config.datasource.jdbcurl";

    private static final String DATABASE_USER = "config.datasource.dbuser";

    private static final String DATABASE_PASSWORD = "config.datasource.dbpassword";

    public static final String SCHEMA_NAME = "config.datasource.schemaname";

    public static final String DEFAULT_SCHEMA_NAME = "public";

    private static DataSource dataSource;

    public DataSourceProvider(Properties configuration) {
        String datasourceJndi = configuration.getProperty(JNDI);
        String schemaName = configuration.getProperty(SCHEMA_NAME);
        if (StringUtils.isBlank(schemaName)) {
            schemaName = DEFAULT_SCHEMA_NAME;
        }

        DataSource ds;
        if (!StringUtils.isEmpty(datasourceJndi)) {
            ds = getDataSourceFromJNDI(datasourceJndi);
        } else {
            ds = createDataSource(
                    configuration.getProperty(JDBC_DRIVER),
                    configuration.getProperty(JDBC_URL),
                    configuration.getProperty(DATABASE_USER),
                    configuration.getProperty(DATABASE_PASSWORD)
            );
        }
        dataSource = new SchemaAwareDataSource(ds, schemaName);
    }

    private DataSource createDataSource(
            final String jdbcDriver,
            final String jdbcUrl,
            final String dbUser,
            final String dbPassword) {
        try {
            Class.forName(jdbcDriver);
        } catch (ClassNotFoundException exception) {
            throw new IllegalArgumentException("Failed to load JDBC_DRIVER: " + jdbcDriver);
        }
        return (DataSource) Proxy.newProxyInstance(
            this.getClass().getClassLoader(),
            new Class<?>[] { DataSource.class },
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (method.getName().equals("toString")) {
                        return "DataSource";
                    }
                    if (!method.getName().equals("getConnection")) {
                        throw new UnsupportedOperationException(method.getName());
                    }
                    return DriverManager.getConnection(jdbcUrl, dbUser, dbPassword);
                }
            }
        );
    }

    private DataSource getDataSourceFromJNDI(String property) {

        DataSource dataSource;
        try {
            InitialContext context = new InitialContext();
            dataSource = (DataSource) context.lookup(property);
        } catch (NamingException e) {
            throw new IllegalArgumentException("Could not lookup datasource.", e);
        }
        return dataSource;
    }

    public DataSource provide() {
        return dataSource;
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/Formatter.java
public class Formatter {

    public static String format(String messagePattern, Object arg) {
        return MessageFormatter.format(messagePattern, arg).getMessage();
    }

    public static String format(String messagePattern, Object arg1, Object arg2) {
        return MessageFormatter.format(messagePattern, arg1, arg2).getMessage();
    }

    public static String format(String messagePattern, Object... args) {
        return MessageFormatter.arrayFormat(messagePattern, args).getMessage();
    }

    /**
     * Escapes string value using PostreSQL dollar quoting
     */
    public static String escapeString(String value) {
        // use dollar-quoted string values to prevent SQL injection
        String dollarQuote = RandomStringUtils.randomAlphabetic(5);
        return String.format(
            "$%s$%s$%s$",
            dollarQuote,
            value,
            dollarQuote
        );
    }

    /**
     * Converts object to string and escapes value using PostgreSQL dollar quoting
     */
    public static String escapeString(Object value) {
        return value == null ? null : escapeString(value.toString());
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/core/EnvelopePBE.java
public class EnvelopePBE {

    private static final String ARTIFACT_KEY = "artifact";
    private static final String VERSION_KEY = "version";
    private static final String ALGORITHM_KEY = "algorithm";
    private static final String SALT_KEY = "salt";
    private static final String ITERATIONS_KEY = "iterations";
    private static final String SECRET_KEY = "secret";

    private static final String ARTIFACT = "EnvelopePBE";
    private static final String VERSION = "1";

    private static final Logger LOG = LoggerFactory.getLogger(EnvelopePBE.class);

    public static boolean check(String blob, String password) throws IOException, GeneralSecurityException {
        final Map<String, String> map = getEncryptionMap(blob);
        if (map.isEmpty()) {
            LOG.warn("Invalid format of previous password. Cannot check equality of previous and new password.");
            return false;
        }

        if (!ARTIFACT.equals(map.get(ARTIFACT_KEY))) {
            throw new IllegalArgumentException(String.format("Invalid artifact '%s'", map.get(ARTIFACT_KEY)));
        }
        if (!VERSION.equals(map.get(VERSION_KEY))) {
            throw new IllegalArgumentException(String.format("Invalid version '%s'", map.get(VERSION_KEY)));
        }

        byte[] salt = Base64.decodeBase64(map.get(SALT_KEY));
        return Arrays.equals(
            Base64.decodeBase64(map.get(SECRET_KEY)),
            SecretKeyFactory.getInstance(map.get(ALGORITHM_KEY)).generateSecret(
                new PBEKeySpec(
                    password.toCharArray(),
                    salt,
                    Integer.parseInt(map.get(ITERATIONS_KEY)),
                    salt.length * 8
                )
            ).getEncoded()
        );
    }

    public static String encode(String algorithm, int keySize, int iterations, String randomProvider, String password) throws IOException, GeneralSecurityException {
        final Base64 base64 = new Base64(0);
        final Map<String, String> map = new HashMap<String, String>();

        byte[] salt = new byte[keySize/8];
        SecureRandom.getInstance(randomProvider == null ? "NativePRNG" : randomProvider).nextBytes(salt);

        map.put(ARTIFACT_KEY, ARTIFACT);
        map.put(VERSION_KEY, VERSION);
        map.put(ALGORITHM_KEY, algorithm);
        map.put(SALT_KEY, base64.encodeToString(salt));
        map.put(ITERATIONS_KEY, Integer.toString(iterations));
        map.put(
            SECRET_KEY,
            base64.encodeToString(
                SecretKeyFactory.getInstance(algorithm).generateSecret(
                    new PBEKeySpec(
                        password.toCharArray(),
                        salt,
                        iterations,
                        salt.length*8
                    )
                ).getEncoded()
            )
        );
        return base64.encodeToString(new ObjectMapper().writeValueAsString(map).getBytes(Charset.forName("UTF-8")));
    }

    public static boolean isFormatCorrect(String blob) throws IOException {
        final Map<String, String> map = getEncryptionMap(blob);
        return ARTIFACT.equals(map.get(ARTIFACT_KEY)) && VERSION.equals(map.get(VERSION_KEY));
    }

    private static Map<String, String> getEncryptionMap(String blob) throws IOException {
        Map<String, String> map;
        try {
            map = new ObjectMapper().readValue(
                    Base64.decodeBase64(blob),
                    new TypeReference<HashMap<String, String>>() {});
        } catch (IOException e) { // password in db wasn't correctly encrypted
            map = Collections.emptyMap();
        }
        return map;
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/DateUtils.java
public class DateUtils {

    public static final int MILLIS_IN_MINUTE = 1000 * 60;
    public static final int MILLIS_IN_HOUR = MILLIS_IN_MINUTE * 60;
    public static final int MILLIS_IN_DAY = MILLIS_IN_HOUR * 24;
    private static final SimpleDateFormat ISO;

    static {
        ISO = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX");
        ISO.setTimeZone(TimeZone.getTimeZone("UTC"));
    }

    public static String toISO(long time) {
        return ISO.format(new Date(time));
    }

    public static long fromISO(String iso) throws ParseException {
        return ISO.parse(iso).getTime();
    }

    /**
     * PostgreSQL to_timestamp() doesn't support timezone specifications, so this is the only way how to set correct
     * timestamp with timezone using string (as our current DAOs uses only strings to create SQL).
     */
    public static String toTimestamp(long time) {
        return String.format(
            "to_timestamp(%d::double precision / 1000)",
            time
        );
    }

    public static Calendar getUtcCalendar() {
        return Calendar.getInstance(
            TimeZone.getTimeZone("UTC"),
            Locale.US
        );
    }

    public static long getLastSunday(long date) {
        Calendar login = Calendar.getInstance();
        login.setTime(new Date(date));

        GregorianCalendar sunday = new GregorianCalendar(
            login.get(Calendar.YEAR),
            login.get(Calendar.MONTH),
            login.get(Calendar.DAY_OF_MONTH),
            0,
            0,
            0
        );
        while (sunday.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
            sunday.add(Calendar.DAY_OF_WEEK, -1);
        }
        return sunday.getTime().getTime();
    }

    public static long add(long date, int field, int amount) {
        Calendar cal = GregorianCalendar.getInstance();
        cal.setTime(new Date(date));
        cal.add(field, amount);
        return cal.getTime().getTime();
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/core/datasource/Sql.java
public class Sql {
    public static class ModificationTypes {
        public static final int INSERT = 0;
        public static final int UPDATE = 1;
        public static final int DELETE = 2;
    }

    public static class ModificationContext {
        public static final ExtKey GENERATED_IDS = new ExtKey("AAA_JDBC_GENERATED_IDS", List/*<Integer>*/.class, "6a587302-bc8d-42fd-8708-30915012f271");
        public static final ExtKey LAST_STATEMENT_ROWS = new ExtKey("AAA_JDBC_LAST_STATEMENT_ROWS", Integer.class, "c1ad321c-ec84-43e4-a8ff-a4f34d1027e7");
    }


    private static final Map<Integer,String> templates = new HashMap<>();
    static {
        templates.put(ModificationTypes.INSERT, "INSERT INTO {}({}) VALUES ({})");
        templates.put(ModificationTypes.UPDATE, "UPDATE {} SET {} {};");
        templates.put(ModificationTypes.DELETE, "DELETE FROM {} {}");
    }

    private static final Logger LOG = LoggerFactory.getLogger(Sql.class);

    public static class Template {
        private final int op;
        private final String table;
        private final List<String> columns = new ArrayList<>();
        private final List<String> values = new ArrayList<>();
        private String where = null;

        public Template(int op, String table) {
            this.op = op;
            this.table = table;
        }

        /**
         * setX invocations are ignored if:
         *
         * <ul>
         * <li>this.op == DELETE statements</li>
         * <li>value == null</li>
         * </ul>
         *
         * @param column column name
         * @param value column value
         * @return this
         */
        public Template setRaw(String column, String value) {
            if (where != null) {
                throw new IllegalStateException();
            }
            if (value != null) {
                columns.add(column);
                values.add(value.toString());
            }
            return this;
        }

        /**
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template#setRaw(String, String)
         */
        public Template setIncrement(String column) {
            return setRaw(column, "(" + column + "+1)");
        }


        /**
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template#setRaw(String, String)
         */
        public Template setString(String column, String value) {
            if (value != null) {
                setRaw(
                    column,
                    Formatter.escapeString(value)
                );
            }
            return this;
        }

        /**
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template#setRaw(String, String)
         */
        public Template setInteger(String column, Integer value) {
            if (value != null) {
                columns.add(column);
                values.add(value.toString());
            }
            return this;
        }

        /**
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template#setString(String, String)
         */
        public Template setLong(String column, Long value){
            if (value != null) {
                columns.add(column);
                values.add(value.toString());
            }
            return this;
        }

        /**
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template#setString(String, String)
         */
        public Template setTimestamp(String column, Long value){
            if (value != null) {
                columns.add(column);
                values.add(DateUtils.toTimestamp(value));
            }
            return this;
        }

        /**
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template#setString(String, String)
         */
        public Template setBool(String column, Boolean value) {
            if (value != null) {
                columns.add(column);
                values.add(value ? "1" : "0");
            }
            return this;
        }

        /**
         * where invocations are ignored if this.op == INSERT statements
         *
         * @param where sql where clause e.g "name = bob"
         * @return this
         */
        public Template where(String where) {
            if (this.where != null) {
                throw new IllegalStateException();
            }
            this.where = where;
            return this;
        }

        public String asSql(){
            List<String> arguments = new ArrayList<>();
            arguments.add(table);
            if (this.op == ModificationTypes.INSERT) {
                arguments.add(StringUtils.join(columns, ", "));
                arguments.add(StringUtils.join(values, ", "));
            } else {
                if (this.op == ModificationTypes.UPDATE){
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < columns.size(); i++) {
                        sb.append(columns.get(i)).append("=").append(values.get(i)).append(",");
                    }
                    if (sb.length() > 0) {
                        sb.setLength(sb.length() - 1);
                    }
                    arguments.add(sb.toString());
                }
                arguments.add(
                    StringUtils.isEmpty(where)?
                        "":
                        "WHERE " + where
                );
            }
            return MessageFormatter.arrayFormat(
                templates.get(op),
                arguments.toArray()
            ).getMessage();
        }
    }

    public static class Modification {

        private final List<String> statements;

        public Modification(String statement) {
            this(Collections.singletonList(statement));
        }

        public Modification(List<String> statements) {
            this.statements = statements;
        }
        public ExtMap execute(DataSource dataSource) throws SQLException {
            return execute(dataSource, true);
        }

        public ExtMap execute(DataSource dataSource, boolean commit) throws SQLException {
            return execute(dataSource.getConnection(), commit);
        }

        /**
         * @param conn a connection to operate on
         * @param commit if true:
         *               1) commit after executing statements
         *               2) attempt to rollback on exception,
         *               3) close when done
         * @return a context containing data on the transaction.
         *
         * @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.ModificationContext
         *
         * @throws SQLException
         */
        public ExtMap execute(Connection conn, boolean commit) throws SQLException {
            List<Integer> generatedIds = new LinkedList<>();
            int lastStatementRet = -1;
            try {
                conn.setAutoCommit(false);
                try (java.sql.Statement statement = conn.createStatement()) {
                    for (String template : statements) {
                        LOG.trace("executing statements: {}", template);
                        lastStatementRet = statement.executeUpdate(template, java.sql.Statement.RETURN_GENERATED_KEYS);

                        ResultSet rs = statement.getGeneratedKeys();
                        if (rs.next()) {
                            generatedIds.add(rs.getInt(1));
                        }
                    }
                    if (commit) {
                        LOG.trace("committing transaction.");
                        conn.commit();
                    }
                }
            } catch (SQLException e) {
                if (commit) {
                    conn.rollback();
                }
                throw e;
            } finally {
                if (commit) {
                    closeQuietly(conn);
                }
            }

            return new ExtMap().mput(
                ModificationContext.GENERATED_IDS,
                generatedIds
            ).mput(
                ModificationContext.LAST_STATEMENT_ROWS,
                lastStatementRet
            );
        }
    }

    public static class Query {
        private String query;

        public Query(String query) {
            this.query = query;
        }

        public <T> T asResults(DataSource dataSource, final ResultsResolver<T> resolver) throws SQLException {
            return asResults(dataSource, resolver, new ExtMap());
        }



        public <T> T asResults(
            DataSource dataSource,
            final ResultsResolver<T> resolver,
            ExtMap context
        ) throws SQLException {
            T resolved;
            Cursor<T> q = null;
            try {
                q = new Query(query).asCursor(dataSource.getConnection(), resolver);
                resolved = resolver.resolve(q.resultSet, context);
            } finally {
                closeQuietly(q);
            }
            return resolved;
        }

        public <T> T asResults(
            Connection conn,
            final ResultsResolver<T> resolver,
            ExtMap context
        ) throws SQLException {
            T resolved;
            Cursor<T> q = null;
            try {
                q = new Query(query).asCursor(conn, resolver);
                resolved = resolver.resolve(q.resultSet, context);
            } finally {
                closeQuietly(q.statement, q.resultSet);
            }
            return resolved;
        }

        public String asString(Connection conn, final String columnName) throws SQLException {
            return asResults(
                conn,
                new ResultsResolver<String>() {
                    @Override
                    public String resolve(ResultSet rs, ExtMap context) throws SQLException {
                        String ret = null;
                        if (rs.next()) {
                            ret = rs.getString(columnName);
                        }
                        return ret;
                    }
                },
                new ExtMap()
            );
        }

        public Integer asInteger(Connection conn, final String columnName) throws SQLException {
            return asResults(
                conn,
                new ResultsResolver<Integer>() {
                    @Override
                    public Integer resolve(ResultSet rs, ExtMap context) throws SQLException {
                        Integer ret = null;
                        if (rs.next()) {
                            ret = rs.getInt(columnName);
                        }
                        return ret;
                    }
                },
                new ExtMap()
            );
        }

        public Integer asInteger(DataSource dataSource, final String columnName) throws SQLException {
            return asResults(
                dataSource,
                new ResultsResolver<Integer>() {
                    @Override
                    public Integer resolve(ResultSet rs, ExtMap context) throws SQLException {
                        Integer ret = null;
                        if (rs.next()) {
                            ret = rs.getInt(columnName);
                        }
                        return ret;
                    }
                },
                new ExtMap()
            );
        }

        public <T> Cursor<T> asCursor(Connection conn, ResultsResolver<T> resolver) throws SQLException{
            return asCursor(conn, resolver, false, new ExtMap());
        }

        public <T> Cursor<T> asCursor(
            Connection conn,
            ResultsResolver<T> resolver,
            boolean scrollable,
            ExtMap ctx
        ) throws SQLException{
            LOG.trace("cursor for: {}", query);
            ResultSet resultSet = null;
            java.sql.Statement statement = null;
            try {
                statement = conn.createStatement(
                    scrollable ? ResultSet.TYPE_SCROLL_INSENSITIVE : ResultSet.TYPE_FORWARD_ONLY,
                    ResultSet.CONCUR_READ_ONLY
                );
                resultSet = statement.executeQuery(query);
                return new Cursor<>(conn, statement, resultSet, ctx, resolver);
            } catch (Exception ex){
                closeQuietly(conn, statement, resultSet);
                throw ex;
            }
        }
    }

    public interface ResultsResolver<T> {
        T resolve(ResultSet resultSet, ExtMap context) throws SQLException;
    }

    public static class Cursor<T> implements AutoCloseable {
        private final Connection connection;
        private final java.sql.Statement statement;
        public final ResultSet resultSet;
        private final ExtMap context;
        private final ResultsResolver<T> resolver;

        public Cursor(
            Connection connection,
            java.sql.Statement statement,
            ResultSet resultSet,
            ExtMap context,
            ResultsResolver<T> resolver
        ) {
            this.connection = connection;
            this.statement = statement;
            this.resultSet = resultSet;
            this.context = context;
            this.resolver = resolver;
        }

        @Override
        public void close() {
            closeQuietly(resultSet, statement, connection);
        }

        public T resolve(ExtMap queryCtx) throws SQLException {
            context.putAll(queryCtx);
            return resolver.resolve(resultSet, context);
        }
    }

    public static void closeQuietly(AutoCloseable... closables) {
        for (AutoCloseable closable : closables) {
            if (closable != null) {
                try {
                    closable.close();
                } catch (Exception e) {
                        LOG.warn("Cannot close AutoCloseable {}, ignoring.", closable.getClass().getName());
                        LOG.debug("Exception", e);
                }
            }
        }
    }

}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/core/Authentication.java
public class Authentication implements Observer {
    public static class AuthRecord {
        public final String principal; // M
        public final long validTo; // M

        public AuthRecord(String principal, long validTo) {
            this.principal = principal;
            this.validTo = validTo;
        }

        @Override
        public String toString() {
            return "AuthRecord{" +
                "principal='" + principal + '\'' +
                ", validTo=" + DateUtils.toISO(validTo) +
                '}';
        }
    }

    public static class AuthResponse {
        public final AuthRecord authRecord;
        public final String principal;
        public final int result; // Only result is mandatory
        public final String dailyMsg;
        public final String baseMsg;

        private final Schema.User user;

        private AuthResponse(
            AuthRecord authRecord,
            Schema.User user,
            int result,
            String dailyMsg,
            String baseMsg
        ) {
            this.authRecord = authRecord;
            this.user = user;
            this.principal = (user == null ? null : user.getName());
            this.result = result;
            this.dailyMsg = dailyMsg;
            this.baseMsg = baseMsg;
        }

        public static AuthResponse negative(int result, Schema.User user, String baseMsg){
            return new AuthResponse(null, user, result, null, baseMsg);
        }

        public static AuthResponse negative(int result){
            return new AuthResponse(null, null, result, null, null);
        }

        public static AuthResponse positive(AuthRecord authRecord, Schema.User user, String dailyMsg){
            return new AuthResponse(authRecord, user, Authn.AuthResult.SUCCESS, dailyMsg, null);
        }

        public static AuthResponse positive() {
            return new AuthResponse(null, null, Authn.AuthResult.SUCCESS, null, null);
        }

        @Override
        public String toString() {
            return "AuthResponse{" +
                "authRecord= " +
                ((authRecord != null)? authRecord.toString(): null) +
                ", principal='" + principal + '\'' +
                ", result=" + result +
                ", dailyMsg='" + dailyMsg + '\'' +
                ", baseMsg='" + baseMsg + '\'' +
                '}';
        }
    }

    private static final Logger LOG = LoggerFactory.getLogger(Authentication.class);
    public static final int MIN_SLEEP = 100;


    static final private Pattern COMPLEXITY_PATTERN = Pattern.compile(
        "(?<name>[^:]*)" +
        ":" +
        "(" +
            "(chars=(?<chars>.*?)::)|" +
            "(min=(?<min>.*?)::)" +
        ")*"
    );


    private final DataSource ds;
    private ExtMap settings;
    private Complexity complexity;
    public Authentication(DataSource ds) {
        this.ds = ds;
    }

    public AuthResponse doAuth(
        String subject,
        String credentials,
        boolean credChange,
        String newCredentials
    ) throws GeneralSecurityException, SQLException, IOException {
        long loginTime = System.currentTimeMillis(); // start the clock
        AuthResponse response = null;

        LOG.debug("Authenticating subject:{} login time:{}", subject, DateUtils.toISO(loginTime));
        try {
            response = authenticate(
                subject,
                credentials,
                loginTime
            );
            if (
                credChange &&
                (
                    response.result == Authn.AuthResult.SUCCESS ||
                    (
                        response.result == Authn.AuthResult.CREDENTIALS_EXPIRED &&
                        settings.get(Schema.Settings.ALLOW_EXPIRED_PASSWORD_CHANGE, Boolean.class)
                    )
                )
            ) {
                AuthResponse credChangeResponse = checkCredChange(response.user, credentials, newCredentials);
                if (credChangeResponse.result == Authn.AuthResult.SUCCESS) {
                    updateUser(
                        new ExtMap().mput(Schema.UserIdentifiers.USER_ID, response.user.getId())
                        .mput(Schema.UserKeys.PASSWORD,
                            EnvelopePBE.encode(
                                settings.get(Schema.Settings.PBE_ALGORITHM, String.class),
                                settings.get(Schema.Settings.PBE_KEY_SIZE, Integer.class),
                                settings.get(Schema.Settings.PBE_ITERATIONS, Integer.class),
                                null,
                                newCredentials
                            )
                        )
                        .mput(Schema.UserKeys.OLD_PASSWORD, response.user.getPassword())
                        .mput(Schema.UserKeys.PASSWORD_VALID_TO,
                            DateUtils.add(
                                loginTime,
                                Calendar.DATE,
                                settings.get(Schema.Settings.PASSWORD_EXPIRATION_DAYS, Integer.class)
                            )
                        )
                    );
                    response = AuthResponse.positive();
                } else {
                    response = credChangeResponse;
                }
            }
            return response;
        } finally {
            if (response == null || response.result != Authn.AuthResult.SUCCESS) {
                delayResponse(loginTime);
            }
        }
    }

    //never return null
    private AuthResponse authenticate(
        String subject,
        String credentials,
        long loginTime
    ) throws GeneralSecurityException, SQLException, IOException {
        AuthResponse response = null;
        Schema.User user = null;

        if (response == null) {
            user = getUser(subject);
            if (user == null) {
                response = AuthResponse.negative(Authn.AuthResult.GENERAL_ERROR);
            }
        }

        synchronized (subject.intern()) { // principal known
            if (response == null) {
                response = isAuthAllowed(user, loginTime);
                if (
                    response == null &&
                    (
                        user.isNopasswd() ||
                        EnvelopePBE.check(user.getPassword(), credentials)
                    )
                ) {
                    response = AuthResponse.positive(
                        new AuthRecord(subject, getValidTo(user, loginTime)),
                        user,
                        getUserMessages(loginTime, user)
                    );
                }
                if (response == null) {
                    response = AuthResponse.negative(Authn.AuthResult.CREDENTIALS_INCORRECT, user, "credentials incorrect");
                }
            }

            if (user != null) { // update db
                if (response.result == Authn.AuthResult.SUCCESS) {
                    updateUser(
                        new ExtMap().mput(Schema.UserIdentifiers.USER_ID, user.getId())
                        .mput(Schema.UserKeys.SUCCESSFUL_LOGIN, loginTime)
                    );
                } else {
                    updateUser(
                        new ExtMap().mput(Schema.UserIdentifiers.USER_ID, user.getId())
                        .mput(Schema.UserKeys.UNSUCCESSFUL_LOGIN, loginTime)
                    );
                    user = getUser(subject);
                    checkLock(user, loginTime);
                }
            }
        }
        return response;
    }

    private Schema.User getUser(String subject) throws SQLException {
        return
            Schema.get(
                new ExtMap().mput(Schema.InvokeKeys.ENTITY, Schema.Entities.USER)
                .mput(Schema.InvokeKeys.DATA_SOURCE, ds)
                .mput(Schema.InvokeKeys.SETTINGS_RESULT, settings)
                .mput(
                    Schema.InvokeKeys.ENTITY_KEYS,
                    new ExtMap().mput(
                        Schema.UserIdentifiers.USERNAME,
                        subject
                    )
                )
            ).get(
                Schema.InvokeKeys.USER_RESULT,
                Schema.User.class
            );
    }

    private void checkLock(Schema.User user, long loginTime) throws SQLException {
        boolean consecutive;
        if (
            (
                consecutive =
                    user.getConsecutiveFailures() >= settings.get(
                        Schema.Settings.MAX_FAILURES_SINCE_SUCCESS,
                        Integer.class
                    )
            ) ||
            (
                user.countFailuresSince(
                    DateUtils.add(
                        loginTime,
                        Calendar.HOUR,
                        -settings.get(Schema.Settings.INTERVAL_HOURS, Integer.class)
                    )
                ) >= settings.get(Schema.Settings.MAX_FAILURES_PER_INTERVAL, Integer.class)
            )
        ) {
            LOG.info(
                "locking user: {} due to {}",
                user.getName(),
                consecutive ?
                "consecutive failures" :
                "interval failures"
            );

            updateUser(
                new ExtMap().mput(Schema.UserIdentifiers.USER_ID, user.getId())
                    .mput(
                        Schema.UserKeys.UNLOCK_TIME,
                        DateUtils.add(
                            loginTime,
                            Calendar.MINUTE,
                            settings.get(Schema.Settings.LOCK_MINUTES, Integer.class)
                        )
                    )
            );
        }
    }

    private void updateUser(ExtMap updateKeys) throws SQLException {
        Schema.modify(
            new ExtMap().mput(Schema.InvokeKeys.ENTITY, Schema.Entities.USER)
                .mput(Schema.InvokeKeys.MODIFICATION_TYPE, Sql.ModificationTypes.UPDATE)
                .mput(Schema.InvokeKeys.DATA_SOURCE, ds)
                .mput(Schema.InvokeKeys.SETTINGS_RESULT, settings)
                .mput(
                    Schema.InvokeKeys.ENTITY_KEYS,
                    updateKeys
                )
        );
    }

    private AuthResponse isAuthAllowed(Schema.User user, long loginTime)  {
        AuthResponse res = null;
        if (user.isDisabled()) {
            res = AuthResponse.negative(Authn.AuthResult.ACCOUNT_DISABLED, user, "account disabled");
        }

        Integer maxAttempts = settings.get(Schema.Settings.MAX_FAILURES_PER_MINUTE, Integer.class);
        if (
            res == null &&
            maxAttempts != Global.SETTINGS_SPECIAL &&
            user.countFailuresSince(
                DateUtils.add(loginTime, Calendar.MINUTE, -1)
            ) >= maxAttempts
        ) {
            res = AuthResponse.negative(Authn.AuthResult.ACCOUNT_RESTRICTION, user, "too many attempts per minute");
        }
        if (
            res == null &&
            !user.isNopasswd() &&
            loginTime > user.getPasswordValidTo()
        ) {
            res = AuthResponse.negative(Authn.AuthResult.CREDENTIALS_EXPIRED, user, "credentials expired");
        }
        if (
            res == null &&
            (
                user.getValidFrom() > loginTime ||
                user.getValidTo() < loginTime
            )
        ) {
            res = AuthResponse.negative(Authn.AuthResult.ACCOUNT_EXPIRED, user, "account expired");
        }
        if (
            res == null &&
            user.getUnlockTime() > loginTime
        ) {
            res = AuthResponse.negative(Authn.AuthResult.ACCOUNT_LOCKED, user, "account locked");
        }
        if (
            res == null &&
            Schema.User.getLoginAllowed(loginTime, user.getLoginAllowed()) == loginTime
        ) {
            res = AuthResponse.negative(Authn.AuthResult.ACCOUNT_TIME_VIOLATION, user, "account time violation");
        }
        return res;
    }

    private String getUserMessages(long loginTime, Schema.User principal) {
        StringBuilder messages = new StringBuilder();
        String separator = settings.get(Schema.Settings.MESSAGE_SEPARATOR, String.class);
        if (settings.get(Schema.Settings.PRESENT_WELCOME_MESSAGE, Boolean.class)) {
            messages.append(principal.getWelcomeMessage())
            .append(separator);

        }
        if (!StringUtils.isEmpty(settings.get(Schema.Settings.MESSAGE_OF_THE_DAY, String.class))) {
            messages.append(settings.get(Schema.Settings.MESSAGE_OF_THE_DAY, String.class))
            .append(separator);
        }
        if (
            settings.get(Schema.Settings.PASSWORD_EXPIRATION_NOTICE_DAYS, Integer.class)
            != Global.SETTINGS_SPECIAL
        ) {
            String expirationMessage =
                principal.getExpirationMessage(
                    loginTime,
                    settings.get(
                        Schema.Settings.PASSWORD_EXPIRATION_NOTICE_DAYS,
                        Integer.class
                    )
            );
            if (!StringUtils.isEmpty(expirationMessage)) {
                messages.append(expirationMessage)
                .append(separator);
            }
        }
        if (messages.length() > 0) {
            messages.setLength(messages.length() - separator.length());
        }
        return messages.toString();
    }

    private void delayResponse(long loginStart) {
        long endTime = DateUtils.add(
            loginStart,
            Calendar.SECOND,
            settings.get(
                Schema.Settings.MINIMUM_RESPONSE_SECONDS,
                Integer.class
            )
        );

        long interval;

        while ((interval = (endTime - System.currentTimeMillis())) > 0) {
            try {
                Thread.sleep(Math.max(MIN_SLEEP, interval));
                break;
            } catch (InterruptedException e) {
                LOG.warn("Interrupted while delaying response, reentering.", e);
            }
        }
    }

    private long getValidTo(Schema.User user, long loginTime) {
        List<Long> timeConstraints = new ArrayList<>(3);
        timeConstraints.add(Schema.User.getLoginAllowed(loginTime, user.getLoginAllowed()));
        timeConstraints.add(user.getValidTo());

        Integer loginMinutes = settings.get(Schema.Settings.MAX_LOGIN_MINUTES, Integer.class);
        if (loginMinutes != Global.SETTINGS_SPECIAL) {
            Calendar globalMax = DateUtils.getUtcCalendar();
            globalMax.setTimeInMillis(loginTime);
            globalMax.add(Calendar.MINUTE, loginMinutes);
            timeConstraints.add(globalMax.getTimeInMillis());
        }
        Collections.sort(timeConstraints);
        return timeConstraints.get(0);
    }

    public AuthResponse checkCredChange(
        Schema.User user,
        String credentials,
        String newCredentials
    ) throws GeneralSecurityException, IOException {
        if (
            !user.isNopasswd() && !EnvelopePBE.check(user.getPassword(), credentials)
        ) {
            return AuthResponse.negative(
                Authn.AuthResult.CREDENTIALS_INCORRECT,
                user,
                "credentials incorrect"
            );
        }

        return checkCredChange(user, newCredentials);
    }

    public AuthResponse checkCredChange(
        Schema.User user,
        String newCredentials
    ) throws GeneralSecurityException, IOException {
        AuthResponse response = null;
        if (newCredentials.length() < settings.get(Schema.Settings.MIN_LENGTH, Integer.class)) {
            response = AuthResponse.negative(
                Authn.AuthResult.GENERAL_ERROR,
                user,
                "new password too short"
            );

        }
        if (response == null && !complexity.check(newCredentials)) {
            response = AuthResponse.negative(
                Authn.AuthResult.GENERAL_ERROR,
                user,
                complexity.getUsage()
            );
        }
        if (response == null && !user.getPassword().equals("") && EnvelopePBE.check(user.getPassword(), newCredentials)) {
            response = AuthResponse.negative(Authn.AuthResult.GENERAL_ERROR, user, "new password already used");
        }
        if (response == null) {
            for (Schema.User.PasswordHistory oldPassword : user.getOldPasswords()) {
                if (!user.getPassword().equals("") && EnvelopePBE.check(oldPassword.password, newCredentials)) {
                    response = AuthResponse.negative(Authn.AuthResult.GENERAL_ERROR, user, "new password already used");
                }
            }
        }
        if (response == null) {
            response = AuthResponse.positive();
        }
        return response;
    }

    @Override
    public void update(Observable o, Object arg) {
        this.settings = (ExtMap)arg;
        Matcher m = COMPLEXITY_PATTERN.matcher(settings.get(Schema.Settings.PASSWORD_COMPLEXITY, String.class));
        boolean ok = true;
        int expectedStart = 0;
        List<Complexity.ComplexityGroup> groups = new ArrayList<>();

        while (m.find()) {
            if (m.start() != expectedStart) {
                throw new RuntimeException("Cannot parse filters");
            }

            groups.add(
                new Complexity.ComplexityGroup(
                    m.group("name"),
                    m.group("chars"),
                    Integer.parseInt(m.group("min"))
                )
            );

            expectedStart = m.end();
            ok = m.end() == m.regionEnd();
        }
        if (!ok) {
            throw new IllegalArgumentException("Cannot parse filters");
        }

        this.complexity = new Complexity(groups);;
        }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/binding/api/ExtensionUtils.java
public class ExtensionUtils {
    public static final ExtMap JDBC_INFO;

    static {
        JDBC_INFO = new ExtMap().mput(
            Base.ContextKeys.AUTHOR,
            "The oVirt Project"
        ).mput(
            Base.ContextKeys.LICENSE,
            "ASL 2.0"
        ).mput(
            Base.ContextKeys.HOME_URL,
            "http://www.ovirt.org"
        ).mput(
            Base.ContextKeys.VERSION,
            Config.PACKAGE_VERSION
        ).mput(
            Base.ContextKeys.EXTENSION_NOTES,
            MessageFormat.format(
                "Display name: {0}",
                Config.PACKAGE_NAME
            )
        ).mput(
            Base.ContextKeys.BUILD_INTERFACE_VERSION,
            Base.INTERFACE_VERSION_CURRENT
        );
    }

    public static Properties loadIncludedConfiguration(ExtMap context) throws IOException {
        try {
            Properties originalConfig =
                context.get(
                    Base.ContextKeys.CONFIGURATION,
                    Properties.class
                );
            Properties expandedConfig = new Properties(originalConfig);

            String baseDir = "/";
            try {
                baseDir = new File(context.get(Base.ContextKeys.CONFIGURATION_FILE, String.class, "/dummy")).getParent();
            } catch (Exception ex) {
                // Ignore
            }

            if (originalConfig.containsKey("config.datasource.file")) {
                expandedConfig.load(
                    new InputStreamReader(
                        new FileInputStream(
                            getRelativeFile(baseDir, originalConfig.getProperty("config.datasource.file"))
                        ),
                        Charset.forName("UTF-8")
                    )
                );
            }
            return expandedConfig;
        } catch (Exception ex) {
            ex.printStackTrace();
            throw ex;
        }
    }

    private static File getRelativeFile(String baseDir, String fileName) {
        File f = new File(fileName);
        if (!f.isAbsolute()) {
            f = new File(baseDir, fileName);
        }
        return f;
    }

    private static Path getDbScriptsDir(String configurationFile) {
        // config file is located in ${PREFIX}/etc/ovirt-engine/extension.d/${NAME}.properties (for API calls) or
        // in ${PREFIX}/etc/ovirt-engine/aaa/${NAME}.properties (for CLI calls)
        Path prefixDir = Paths.get(configurationFile).normalize().getParent().getParent().getParent().getParent();
        if (prefixDir.getNameCount() == 0) {
            // RPM installation, ${PREFIX} should be /usr instead of /
            prefixDir = Paths.get("/usr");
        }
        // dbscripts are located in ${PREFIX}/share/ovirt-engine-extension-aaa-jdbc/dbscripts/upgrade
        return Paths.get(prefixDir.toString(), "share/ovirt-engine-extension-aaa-jdbc/dbscripts/upgrade");

    }

    private static String getLatestUpgradeScriptName(Path upgradeScriptsDir) throws IOException {
        List<String> fileNames = new ArrayList<>();
        DirectoryStream.Filter<Path> fileFilter = new DirectoryStream.Filter<Path>() {
            public boolean accept(Path file) throws IOException {
                return !Files.isDirectory(file);
            }
        };
        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(upgradeScriptsDir, fileFilter)) {
            for (Path path : directoryStream) {
                // in schema_version table script name contains "upgrade/" prefix
                fileNames.add(path.subpath(path.getNameCount() - 2, path.getNameCount()).toString());
            }
        } finally {
        }
        Collections.sort(fileNames);
        return fileNames.get(fileNames.size() -1);
    }

    public static void checkDbVersion(DataSource ds, String configurationFile)
    throws IOException, SQLException {
        try (Connection conn = ds.getConnection()) {
            boolean uptodate = new Sql.Query(
                Formatter.format(
                    "SELECT COUNT(script) AS count FROM schema_version WHERE script = {}",
                    Formatter.escapeString(
                        getLatestUpgradeScriptName(
                            getDbScriptsDir(configurationFile)
                        )
                    )
                )
            ).asInteger(conn, "count") == 1;
            if (!uptodate) {
                throw new RuntimeException(
                    "Database schema is older than required by currently installed ovirt-engine-extension-aaa-jdbc " +
                    "package version. Please upgrade profile database schema before proceeding (for more info about " +
                    "upgrade please take a look at README.admin file contained in ovirt-engine-extension-aaa-jdbc " +
                    "package)."
                );
            }
        }
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/Global.java
public class Global {
    public static final int SETTINGS_SPECIAL = -1;
    public static final String NAMESPACE = "*";

    public static class SearchContext {
        /** can be principal or group */
        public static final ExtKey IS_PRINCIPAL = new ExtKey("AAA_JDBC_IS_PRINCIPAL", Boolean.class, "c991a616-7526-403c-a6ff-89578ef4351d");
        public static final ExtKey WITH_GROUPS = new ExtKey("AAA_JDBC_WITH_GROUPS", Boolean.class, "2161fc6c-9c83-4865-b331-39189d3858ea");
        public static final ExtKey RECURSIVE = new ExtKey("AAA_JDBC_RECURSIVE", Boolean.class, "3dbd6bcc-64c3-4675-aadb-9afdd6ab6966");
        public static final ExtKey PAGE_SIZE = new ExtKey("AAA_JDBC_PAGE_SIZE", Integer.class, "e0fe28ad-24c0-4d29-8868-5c639701f4b5");
        public static final ExtKey ALL_ATTRIBUTES = new ExtKey("AAA_JDBC_RETURN_ALL_ATTRIBUTES", Boolean.class, "2460d328-f7b0-4da5-9621-6d3051dbd433");
    }

    public static class InvokeKeys {
        /** @see org.ovirt.engine.extension.aaa.jdbc.Global.SearchContext */
        public static final ExtKey SEARCH_CONTEXT = new ExtKey("AAA_JDBC_SEARCH_CONTEXT", ExtMap.class, "bc9b1f3e-6d0e-4ce7-bade-507254815c49");
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/binding/cli/command/Command.java
public abstract class Command {
    private static final Logger LOG = LoggerFactory.getLogger(Command.class);

    public abstract String getName();
    public List<String> getSubModules() {
        return Collections.emptyList();
    }
    /** is a positional argument expected before the module options. */
    public boolean entityNameExpected() {
        return false;
    }
    /**
     * This method is responsible for:
     * - parsing preceding positional argument expected before the keyword arguments according to getPosCount().
     * - parsing keyword arguments according to getName()
     * if getSubModules().size() &gt; 1:
     * - parsing name of next expected command and invoking it
     */
    public abstract void invoke(ExtMap context, Map<String, Object> args);

    public void invoke(ExtMap context) {
        if (entityNameExpected()) { // e.g user add 'mike'
            putNextPositional(context);
            LOG.trace("entity name is: {}", context.<String>get(ContextKeys.POSITIONAL));
        }
        Map<String, Object> args = null;

        if (!getName().startsWith("_")) { // get keyword args
            args = nextArgs(context, context.containsKey(ContextKeys.EXIT_STATUS));
        }

        if (!context.containsKey(ContextKeys.EXIT_STATUS)) { // invoke command
            LOG.trace("Invoking: {} with args: {}", getName(), args);

            invoke(context, args);
        }

        if (!context.containsKey(ContextKeys.EXIT_STATUS) && getSubModules().size() > 0) { // get next
            putNextPositional(context);
            String name = context.get(ContextKeys.POSITIONAL, String.class);
            LOG.trace("next: {}", name);

            if (
                !this.getSubModules().contains(name)
            ) {
                addContextMessage(context, true,
                     Formatter.format(
                         "{}. {}\n",
                         (
                             name != null ?
                             "unexpected command: " + name :
                             "no command provided"
                         ),
                         "options: " + StringUtils.join(getSubModules(), ", ") + ", help")
                );
                context.putIfAbsent(ContextKeys.EXIT_STATUS, Cli.ARGUMENT_PARSING_ERROR);
            }
            if (!context.containsKey(ContextKeys.EXIT_STATUS)) {
                context.remove(ContextKeys.POSITIONAL);
                Cli.getCommands().get(this.getName() + "-" + name).invoke(context);
            }
        }
    }

    private void putNextPositional(ExtMap context) {
        String name;
        @SuppressWarnings("unchecked")
        List<String> tail = context.get(ContextKeys.TAIL, List.class);
        if (tail.size() > 0 && !tail.get(0).startsWith("--")) {
            name = tail.remove(0);
            context.put(ContextKeys.POSITIONAL, name);
            context.put(ContextKeys.TAIL, tail);
        } else {
            context.put(ContextKeys.EXIT_STATUS, Cli.ARGUMENT_PARSING_ERROR);
        }
    }

    private Map<String, Object> nextArgs(ExtMap context, boolean showHelp) {
        Map<String, String> contextSubstitutions = new HashMap<>();
        contextSubstitutions.put("@ENGINE_ETC@", System.getProperty("org.ovirt.engine.aaa.jdbc.engineEtc"));
        contextSubstitutions.put("@PROGRAM_NAME@", System.getProperty("org.ovirt.engine.aaa.jdbc.programName"));
        contextSubstitutions.put("@MODULE_LIST@", StringUtils.join(this.getSubModules(), "\n  ") + "\n  help");

        Map<String, Object> parsed;
        ArgumentsParser argumentsParser = new ArgumentsParser(
            Cli.class.getResourceAsStream("arguments.properties"),
            this.getName()
        );
        argumentsParser.getSubstitutions().putAll(contextSubstitutions);
        @SuppressWarnings("unchecked")
        List<String> tail = context.get(ContextKeys.TAIL, List.class);
        argumentsParser.parse(tail); // updates tail.
        parsed = argumentsParser.getParsedArgs();

        if (showHelp || !context.containsKey(ContextKeys.EXIT_STATUS)) {

            if (showHelp || (Boolean)parsed.get("help") ||  (tail.size() > 0 && tail.get(0).equals("help"))) {
                addContextMessage(context, false, argumentsParser.getUsage());
                context.putIfAbsent(ContextKeys.EXIT_STATUS, Cli.SUCCESS);
            } else {
                List<Throwable> errors = argumentsParser.getErrors();
                if (errors.size() > 0) {
                    for (Throwable thr: errors) {
                        context.<List<Throwable>>get(ContextKeys.THROWABLES).add(thr);
                        context.<List<String>>get(ContextKeys.ERR_MESSAGES).add(thr.getMessage());
                    }
                    context.mput(ContextKeys.EXIT_STATUS, Cli.ARGUMENT_PARSING_ERROR);
                }
            }
        }
        return parsed;
    }

    protected static void addContextMessage(ExtMap context, boolean error, String message) {
        ExtKey key;
        if (error) {
            key = ContextKeys.ERR_MESSAGES;
        } else {
            key = ContextKeys.OUT_MESSAGES;
        }
        context.<List<String>>get(key).add(message);
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/core/Schema.java
public class Schema {
    public static class Entities {
        public static final ExtUUID SETTINGS = new ExtUUID("AAA_JDBC_SETTINGS", "761445ec-fe4e-4e1d-9f12-8ca6d9a66a23");
        /**
         * Authn view of a user.
         * @see Schema.User
         * */
        public static final ExtUUID USER = new ExtUUID("AAA_JDBC_USER", "74cead40-a5f4-41b2-b74d-c0f961a6e66e");
        /** Group  */
        public static final ExtUUID GROUP = new ExtUUID();

        /** Group type members of Group */
        public static final ExtUUID GROUP_MEMBERSHIPS = new ExtUUID(
                "AAA_JDBC_GROUP_MEMBERSHIPS",
                "7dbb74df-9107-49a6-9487-bcee8a1c52d5"
        );

        /** A cursor for a SEARCH_PAGE */
        public static final ExtUUID CURSOR = new ExtUUID("AAA_JDBC_CURSOR", "ce383ef3-1f3f-41c2-95e0-d9cc4ec5fbb6");
        /**
         * Authz view of a collection of Principals or Groups depending on cursor used to obtain page.
         * Contains information from (users, user_attributes, user_groups) OR
         * (groups, group_attributes and group_groups). Must first acquire a cursor.
         */
        public static final ExtUUID SEARCH_PAGE = new ExtUUID("AAA_JDBC_ENTITIES_PAGE", "fe1d2d6c-0595-4eab-b243-feec4baacf13");
    }

    public static class InvokeKeys {
        /** @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.Entities */
        public static final ExtKey ENTITY = new ExtKey("AAA_JDBC_CLI_ENTITY", ExtUUID.class,"40cdb194-8393-4930-9906-b356cfedf6d1");
        /**
         *
         * @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.UserKeys
         * @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.UserIdentifiers
         * @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.GroupKeys
         * @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.GroupIdentifiers
         * @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.CursorKeys
         * @see org.ovirt.engine.extension.aaa.jdbc.core.Schema.Settings
         */
        public static final ExtKey ENTITY_KEYS = new ExtKey("AAA_JDBC_ENTITY_KEYS", ExtMap.class,"632b76ad-aac4-4c97-b3c5-7b1ffa707a21");

        /** @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.ModificationTypes */
        public static final ExtKey MODIFICATION_TYPE = new ExtKey("AAA_JDBC_MODIFICATION_TYPE", Integer.class, "c321aedb-ae00-4ec8-a478-32426da7d4d2");
        public static final ExtKey USER_RESULT = new ExtKey("AAA_JDBC_USER_RESULT", User.class, "4012d9b1-735a-43eb-9bc1-c065c9638d70");
        public static final ExtKey SETTINGS_RESULT = new ExtKey("AAA_JDBC_SETTINGS_RESULT", ExtMap.class, "966f32e0-7154-4736-8890-a05a694a9cd0");
        public static final ExtKey GROUP_MEMBERSHIPS_RESULT = new ExtKey("AAA_JDBC_GROUP_MEMBERSHIPS_RESULT", List.class, "5e8d3e48-ed12-407b-95e2-a18c6bccb5d4");
        public static final ExtKey CURSOR_RESULT = new ExtKey("AAA_JDBC_CURSOR_RESULT", Sql.Cursor.class, "016aaa8b-8ae2-4220-a277-8197e4eb5405");
        /** SEARCH_PAGE_RESULT is null if there are no results */
        public static final ExtKey SEARCH_PAGE_RESULT = new ExtKey("AAA_JDBC_PRINCIPAL_RESULT", Collection /**<ExtMap.class*/.class, "7a5c3151-1160-4668-b34c-0062997fc7af");
        public static final ExtKey DATA_SOURCE = new ExtKey("AAA_JDBC_DATA_SOURCE", DataSource.class, "80564c29-c9cf-499d-ba7f-c0db66f426e7");
    }

    public static class SharedKeys {
        public static final ExtKey ATTRIBUTE_NAME = new ExtKey("CATALOG_ATTRIBUTE_NAME", String.class, "d4396c43-74f1-44a3-9bd6-d5afb3b6bceb");
        public static final ExtKey ATTRIBUTE_VALUE = new ExtKey("CATALOG_ATTRIBUTE_VALUE", String.class, "17ac4a7f-cea6-453e-8b38-b1ed218984d3");

        public static final ExtKey ADD_GROUP = new ExtKey("CATALOG_ADD_GROUP", String.class, "60cfb4ad-5f23-4feb-9fe5-bda4d1a92aef");
        public static final ExtKey REMOVE_GROUP = new ExtKey("CATALOG_REMOVE_GROUP", String.class, "718b4a9b-8dfb-4537-aa4e-d96ed36e03cc");
        public static final ExtKey ATTRIBUTES = new ExtKey("CATALOG_USER_ATTRIBUTES", Collection/*<ExtMap>*/.class, "c1e68323-dc2d-47c3-b9aa-a830c5ef3eec");
    }

    public static class UserIdentifiers {
        /** one of these must be provided. */
        public static final ExtKey USERNAME = new ExtKey("CATALOG_USERNAME", String.class, "4379174b-00ca-4123-98c3-e7e6956756fc");
        public static final ExtKey USER_ID = new ExtKey("CATALOG_ID", Integer.class, "7e067ee4-49c0-4ece-99ae-51dcd75e8988");
    }
    public static class UserKeys {
        public static final ExtKey UUID = new ExtKey("CATALOG_EXTERNAL_ID", String.class, "328c1fdd-fe5d-40c3-b861-c4287b8c9ea2");
        public static final ExtKey NEW_USERNAME = new ExtKey("CATALOG_NEW_USERNAME", String.class, "03c43642-093c-473d-906f-9a0453617169");
        /** change password if complexity and password history allow, insert a user_password_history record */
        public static final ExtKey PASSWORD = new ExtKey("CATALOG_PASSWORD",  String.class, "22307c2e-c7f2-4339-b392-8f3086d4c737", ExtKey.Flags.SENSITIVE);
        /** needed for pass change */
        public static final ExtKey OLD_PASSWORD = new ExtKey("CATALOG_PLD_PASSWORD",  String.class, "6b69d8c8-5a7e-44fb-83ae-a059ba10d5d6", ExtKey.Flags.SENSITIVE);
        public static final ExtKey PASSWORD_VALID_TO = new ExtKey("CATALOG_PASSWORD_VALID_TO", Long.class, "0997d4e7-13d9-4a86-85d4-e9499d8de56e");
        public static final ExtKey VALID_FROM = new ExtKey("CATALOG_VALID_FROM", Long.class, "5bae23fe-b661-48fe-ad6d-321c50542857");
        public static final ExtKey VALID_TO = new ExtKey("CATALOG_VALID_TO", Long.class, "5a7514b0-c670-46cf-8822-c0aedd8ccce8");
        public static final ExtKey LOGIN_ALLOWED = new ExtKey("CATALOG_LOGIN_ALLOWED", String.class, "1e932817-f863-4e49-bb44-a823be9ef4fe");
        public static final ExtKey NOPASS = new ExtKey("CATALOG_NOPASSWD", Boolean.class, "dc85f1d8-0933-4f15-b037-ef4007229436");
        public static final ExtKey FORCE_PASSWORD = new ExtKey("CATALOG_FORCE_PASSWORD", Boolean.class, "9b187ad5-b403-4412-bf68-debc7fcc17a6");
        public static final ExtKey DISABLED = new ExtKey("CATALOG_DISABLED", Boolean.class, "7a5d77c6-f831-400b-bc8e-b9d8ea286408");
        /** unlock_time = value,  consecutive_failures = 0 */
        public static final ExtKey UNLOCK_TIME = new ExtKey("CATALOG_UNLOCK_TIME", Long.class, "7b1042a6-35ea-4108-8cc4-2ed142ba002f");
        public static final ExtKey LOCKED = new ExtKey("CATALOG_LOCKED", Boolean.class, "df080800-6460-41ad-a3d4-eec98cf1c7d0");
        /**  last_successful_login = value, consecutive_failures = 0 */
        public static final ExtKey SUCCESSFUL_LOGIN = new ExtKey("CATALOG_SUCCESSFUL_LOGIN", Long.class, "338117a2-4eec-4aca-9dd1-9bcbe95600f2");
        /**  last_unsuccessful_login = value, consecutive_failures++, insert a failed_login record */
        public static final ExtKey UNSUCCESSFUL_LOGIN = new ExtKey("CATALOG_SUCCESSFUL_LOGIN", Long.class, "c10a40f8-5fed-4908-a27e-ec9b0ae83324");
    }

    public static class GroupIdentifiers {
        public static final ExtKey NAME = new ExtKey("CATALOG_GROUP_NAME", String.class, "6e3c6b25-4999-4602-9ce8-4e1c278ecb3b");
    }

    public static class GroupKeys {
        /** Database ID of the group. **/
        public static final ExtKey DB_ID = new ExtKey("CATALOG_GROUP_DB_ID", Integer.class, "30f9b8e8-66bd-4744-ba56-b1457e38d214");
        public static final ExtKey UUID = new ExtKey("CATALOG_GROUP_ID", String.class, "6cb2a99b-5597-4335-8a88-9cd4309f778d");
        public static final ExtKey NEW_NAME = new ExtKey("CATALOG_GROUP_NEW_NAME", String.class, "81c2bbfb-189a-485d-8330-dd46cfd14f2e");
    }

    public static class CursorKeys {
        /** needed to create a cursor */
        public static final ExtKey FILTER = new ExtKey("AAA_JDBC_FILTER", String.class, "632b76ad-aac4-4c97-b3c5-7b1ffa707a21");
    }

    /** Note: names of loaded keys are retrieved from the database. */
    public static class Settings {
        /**
         * Authentication related
         */
        public static final ExtKey PASSWORD_HISTORY_LIMIT = new ExtKey("", Integer.class, "e843bc2a-0878-4b6f-9be3-32e83169fb7c");
        public static final ExtKey LOCK_MINUTES = new ExtKey("", Integer.class, "78b5138a-d52b-464d-a2a7-5fed55bdf7b3");
        public static final ExtKey PRESENT_WELCOME_MESSAGE = new ExtKey("", Boolean.class, "0ae5affd-15e5-4bb1-9910-f091b64b7197");
        public static final ExtKey MESSAGE_SEPARATOR = new ExtKey("", String.class, "ecf6d62a-10f8-4fad-b401-75c9d0788955");
        public static final ExtKey MESSAGE_OF_THE_DAY = new ExtKey("", String.class, "df496823-6814-4720-a302-c723f629f847");
        public static final ExtKey PASSWORD_EXPIRATION_DAYS = new ExtKey("", Integer.class, "dc3e2fb4-cbcc-4f5c-9b06-5db9ec534aa8");
        public static final ExtKey PASSWORD_EXPIRATION_NOTICE_DAYS = new ExtKey("", Integer.class, "69d5cec2-bd1a-42e1-84f0-05627ee476b3");
        public static final ExtKey MAX_FAILURES_PER_MINUTE = new ExtKey("", Integer.class, "d9fce842-1906-40c0-b8d0-f01d2454623b");
        public static final ExtKey MAX_LOGIN_MINUTES = new ExtKey("", Integer.class, "bb90c43c-45cb-4af9-8c3e-4f6dee6ba60b");
        public static final ExtKey MINIMUM_RESPONSE_SECONDS = new ExtKey("", Integer.class, "1a496c2f-02d7-4057-bfeb-23019d3941ae");
        public static final ExtKey MAX_FAILURES_SINCE_SUCCESS = new ExtKey("", Integer.class, "d03f5b67-5dca-4730-b1ac-01a6e8f31f25");
        public static final ExtKey MAX_FAILURES_PER_INTERVAL = new ExtKey("", Integer.class, "3b1e221c-21d2-45c9-95c5-2f71deec3b9f");
        public static final ExtKey INTERVAL_HOURS = new ExtKey("", Integer.class, "756f2a73-be20-4103-b82f-f41dae35f8bf");
        public static final ExtKey ALLOW_EXPIRED_PASSWORD_CHANGE = new ExtKey("", Boolean.class, "aaa93f69-7b75-44ee-b8a7-4d4736e73be1");
        public static final ExtKey PASSWORD_COMPLEXITY = new ExtKey("", String.class, "b55243d1-27b5-49bf-8436-67d5ada33975");
        public static final ExtKey PBE_ALGORITHM = new ExtKey("", String.class, "8669297e-cfd7-45e3-80cd-a464c90694d7");
        public static final ExtKey PBE_KEY_SIZE = new ExtKey("", Integer.class, "8aa785db-bd8e-4ddd-9590-185b8a870f6f");
        public static final ExtKey PBE_ITERATIONS = new ExtKey("", Integer.class, "fa528912-4f3a-4c3a-8e93-561e92d785e8");
        public static final ExtKey MIN_LENGTH = new ExtKey("", Integer.class, "24e7de2f-a714-4f3e-8f64-13bc6ee7525b");
        /**
         * Authorization keys
         */
        public static final ExtKey MAX_PAGE_SIZE  = new ExtKey("", Integer.class, "8a077fb6-7271-4d79-a0b5-aa9a84384b69");
        /**
         * Tasks keys
         */
        public static final ExtKey REFRESH_SETTINGS_INTERVAL_MINUTES = new ExtKey("", Long.class, "fba813af-4c30-448f-8df9-9334449c149e");
        public static final ExtKey HOUSE_KEEPING_INTERVAL_HOURS = new ExtKey("", Long.class, "d89abf58-f0ca-4be8-8ec3-45a1f3645c7b");
        public static final ExtKey FAILED_LOGINS_OLD_DAYS  = new ExtKey("", Integer.class, "12fb6fee-797b-47e7-83e1-1f1fd8b8dd05");
        /**
         * Meta
         */
        public static final ExtKey SETTING_DESCRIPTIONS  = new ExtKey("SETTINGS_DESCRIPTIONS", ExtMap.class, "4e085d97-53d9-4ba8-9303-6df3b060e580");
    }

    public static class AuthzInternal {
        public static final ExtKey USER_DESCRIPTION = new ExtKey("AAA_JDBC_USER_DESCRIPTION", String.class, "5b1d284d-1688-403b-9e0a-af856961532c");
        public static final ExtKey GROUP_DESCRIPTION = new ExtKey("AAA_JDBC_GROUP_DESCRIPTION", String.class, "e13dcd41-dcf7-483b-9cac-5473c6f9d8e8");
        public static final Integer LIKE = 1000;
    }

    private static class SettingsResolver implements Sql.ResultsResolver<ExtMap> {
        @Override
        public ExtMap resolve(ResultSet rs, ExtMap ctx) throws SQLException {
            ExtMap settings = new ExtMap();
            ExtMap descriptions = new ExtMap();
            UUID uuid;
            ExtKey key = null;
            String value = null;
            try {
                while (rs.next()) {
                    uuid = UUID.fromString(rs.getString("uuid"));
                    key = SETTING_KEYS.get(uuid);
                    value = rs.getString("value");
                    if (key != null) {
                        settings.put(
                            new ExtKey(rs.getString("name"), key.getType(), key.getUuid().getUuid().toString()),
                            key.getType().getConstructor(java.lang.String.class).newInstance(value)
                        );
                        descriptions.put(
                            new ExtKey(rs.getString("name"), String.class, key.getUuid().getUuid().toString()),
                            rs.getString("description")
                        );
                    } else {
                        throw new RuntimeException("key for " + uuid + " not found");
                    }
                }
            } catch ( NoSuchMethodException|InvocationTargetException|InstantiationException|IllegalAccessException e) {
                throw new RuntimeException(
                    Formatter.format(
                        "Could not convert setting to expected type. value: {} requested type: {}",
                        value,
                        key.getType()
                    ),
                    e
                );
            }
            return settings.mput(Settings.SETTING_DESCRIPTIONS, descriptions);
        }
    }

    private static class GroupMembersOfGroupResolver implements Sql.ResultsResolver<List<ExtMap>> {
        @Override
        public List<ExtMap> resolve(ResultSet rs, ExtMap context) throws SQLException {
            List<ExtMap> groupRecords = new ArrayList<>();
            while (rs.next()) {
                ExtMap groupRecord = new ExtMap();
                groupRecord.put(Authz.GroupRecord.NAMESPACE, rs.getString("namespace"));
                groupRecord.put(Authz.GroupRecord.ID, rs.getString("group_uuid"));
                groupRecord.put(Authz.GroupRecord.NAME, rs.getString("group_name"));
                groupRecord.put(Authz.GroupRecord.DISPLAY_NAME, rs.getString("group_display_name"));
                groupRecord.putIfAbsent(Authz.GroupRecord.DISPLAY_NAME, "");
                groupRecord.put(AuthzInternal.GROUP_DESCRIPTION, rs.getString("group_description"));
                groupRecord.putIfAbsent(AuthzInternal.GROUP_DESCRIPTION, "");
                groupRecord.put(Authz.GroupRecord.GROUPS, new ArrayList<ExtMap>());
                if (context.get(Global.SearchContext.ALL_ATTRIBUTES, Boolean.class, false)) {
                    groupRecord.put(GroupKeys.DB_ID, rs.getInt("group_id"));
                }
                if (StringUtils.isNotBlank(groupRecord.<String>get(Authz.GroupRecord.ID))) {
                    // due to "select '*' as namespace" we receive also empty records, so we need to ignore them
                    groupRecords.add(groupRecord);
                }
            }
            return groupRecords;
        }
    }

    public static class
        searchPageResolver implements Sql.ResultsResolver<Collection<ExtMap>> {
        @Override
        public Collection<ExtMap> resolve(ResultSet rs, ExtMap context) throws SQLException {
            Collection<ExtMap> page = null;
            ExtMap next;
            do {
                next = nextPage(rs, context);
                if (next != null) {
                    if (page == null) {
                        page = new ArrayList<>();
                    }
                    page.add(next);
                }
            } while (next != null && page.size() < context.get(Global.SearchContext.PAGE_SIZE, Integer.class));
            return page;
        }

        private static ExtMap nextPage(ResultSet rs, ExtMap context) throws SQLException {
            String entityId = null;
            Boolean principal = context.get(Global.SearchContext.IS_PRINCIPAL, Boolean.class);
            String idName = principal ? "user_id" : "group_id";
            boolean reachedNext = false;
            ExtMap page = null;
            while (!reachedNext && rs.next()) {
                if (entityId == null || entityId.equals(rs.getString(idName))) { // same entity, add row!
                    if (page == null){
                        page = new ExtMap();
                    }
                    if (principal) {
                        putNextPrincipal(rs, page, context);
                    } else {
                        putNextGroup(rs, page, context);
                    }
                    entityId = rs.getString(idName);
                } else {
                    reachedNext = true;
                    rs.relative(-1);
                }
             }
            return page;
        }
    }

    public static void putNextPrincipal(
        ResultSet rs,
        ExtMap next,
        ExtMap context
    ) throws SQLException {
        if (!next.containsKey(Authz.PrincipalRecord.NAMESPACE)) {
            next.put(Authz.PrincipalRecord.NAMESPACE, rs.getString("namespace"));
            next.put(Authz.PrincipalRecord.ID, rs.getString("user_uuid"));
            next.put(Authz.PrincipalRecord.NAME, rs.getString("user_name"));
            next.put(Authz.PrincipalRecord.PRINCIPAL, rs.getString("user_name"));
            next.put(Authz.PrincipalRecord.DISPLAY_NAME, rs.getString("user_display_name"));
            next.putIfAbsent(Authz.PrincipalRecord.DISPLAY_NAME, "");
            next.put(AuthzInternal.USER_DESCRIPTION, rs.getString("user_description"));
            next.putIfAbsent(AuthzInternal.USER_DESCRIPTION, "");
            next.put(Authz.PrincipalRecord.EMAIL, rs.getString("user_email"));
            next.putIfAbsent(Authz.PrincipalRecord.EMAIL, "");
            next.put(Authz.PrincipalRecord.FIRST_NAME, rs.getString("user_first_name"));
            next.putIfAbsent(Authz.PrincipalRecord.FIRST_NAME, "");
            next.put(Authz.PrincipalRecord.LAST_NAME, rs.getString("user_last_name"));
            next.putIfAbsent(Authz.PrincipalRecord.LAST_NAME, "");
            next.put(Authz.PrincipalRecord.DEPARTMENT, rs.getString("user_department"));
            next.putIfAbsent(Authz.PrincipalRecord.DEPARTMENT, "");
            next.put(Authz.PrincipalRecord.TITLE, rs.getString("user_title"));
            next.putIfAbsent(Authz.PrincipalRecord.TITLE, "");
            next.put(Authz.PrincipalRecord.GROUPS, new ArrayList<ExtMap>());
            if (context.get(Global.SearchContext.ALL_ATTRIBUTES, Boolean.class, false)) {
                next.put(UserKeys.DISABLED, rs.getBoolean("user_disabled"));
                next.put(UserKeys.UNLOCK_TIME, rs.getTimestamp("user_unlock_time").getTime());
                next.put(UserKeys.LOCKED, (Long)next.get(UserKeys.UNLOCK_TIME) > System.currentTimeMillis());
                next.put(UserKeys.VALID_FROM, rs.getTimestamp("user_valid_from").getTime());
                next.put(UserKeys.VALID_TO, rs.getTimestamp("user_valid_to").getTime());
                next.put(UserKeys.NOPASS, rs.getBoolean("user_nopasswd"));
                next.put(UserKeys.SUCCESSFUL_LOGIN, rs.getTimestamp("user_last_successful_login").getTime());
                next.put(UserKeys.UNSUCCESSFUL_LOGIN, rs.getTimestamp("user_last_unsuccessful_login").getTime());
                next.put(UserKeys.PASSWORD_VALID_TO, rs.getTimestamp("user_password_valid_to").getTime());
            }
        }

        if (context.get(Global.SearchContext.WITH_GROUPS, Boolean.class)) {
            String groupId = rs.getString("user_group_id");
            if (groupId != null) {
                ExtMap group = new ExtMap();
                group.put(Authz.GroupRecord.NAMESPACE, rs.getString("namespace"));
                group.put(Authz.GroupRecord.ID, rs.getString("user_group_uuid"));
                group.put(Authz.GroupRecord.NAME, rs.getString("user_group_name"));
                group.put(Authz.GroupRecord.DISPLAY_NAME, rs.getString("user_group_display_name"));
                group.putIfAbsent(Authz.GroupRecord.DISPLAY_NAME, "");
                group.put(AuthzInternal.GROUP_DESCRIPTION, rs.getString("user_group_description"));
                group.putIfAbsent(AuthzInternal.GROUP_DESCRIPTION, "");


                next.<List<ExtMap>>get(Authz.PrincipalRecord.GROUPS).add(group);
            }
        }
    }

    private static void putNextGroup(
        ResultSet rs,
        ExtMap next,
        ExtMap context
    ) throws SQLException {
        if (!next.containsKey(Authz.GroupRecord.NAMESPACE)) {
            next.put(Authz.GroupRecord.NAMESPACE, rs.getString("namespace"));
            next.put(Authz.GroupRecord.ID, rs.getString("group_uuid"));
            next.put(Authz.GroupRecord.NAME, rs.getString("group_name"));
            next.put(Authz.GroupRecord.DISPLAY_NAME, rs.getString("group_display_name"));
            next.putIfAbsent(Authz.GroupRecord.DISPLAY_NAME, "");
            next.put(AuthzInternal.GROUP_DESCRIPTION, rs.getString("group_description"));
            next.putIfAbsent(AuthzInternal.GROUP_DESCRIPTION, "");
            next.put(Authz.GroupRecord.GROUPS, new ArrayList<ExtMap>());
            if (context.get(Global.SearchContext.ALL_ATTRIBUTES, Boolean.class, false)) {
                next.put(GroupKeys.DB_ID, rs.getInt("group_id"));
            }
        }
        if (context.get(Global.SearchContext.WITH_GROUPS, Boolean.class)) {
            String nested = rs.getString("group_group_id");
            if (nested != null) {
                ExtMap group = new ExtMap();
                group.put(Authz.GroupRecord.NAMESPACE, rs.getString("namespace"));
                group.put(Authz.GroupRecord.ID, rs.getString("group_group_uuid"));
                group.put(Authz.GroupRecord.NAME, rs.getString("group_group_name"));
                group.put(Authz.GroupRecord.DISPLAY_NAME, rs.getString("group_group_display_name"));
                group.putIfAbsent(Authz.GroupRecord.DISPLAY_NAME, "");
                group.put(AuthzInternal.GROUP_DESCRIPTION, rs.getString("group_group_description"));
                group.putIfAbsent(AuthzInternal.GROUP_DESCRIPTION, "");

                next.<List<ExtMap>>get(Authz.GroupRecord.GROUPS).add(group);
            }
        }
    }


    /**
     * Authn view of a user.
     */
    public static class User {
        private static class UserResolver implements Sql.ResultsResolver<User> {
            @Override
            public User resolve(ResultSet rs, ExtMap context) throws SQLException {
                User user = null;
                if (rs.next()) {
                    user = new User();
                    user.id = rs.getInt("u_id");
                    user.name = rs.getString("u_name");
                    user.password = rs.getString("u_password");
                    user.passwordValidTo = rs.getTimestamp("u_password_valid_to").getTime();
                    user.loginAllowed = rs.getString("u_login_allowed");
                    user.disabled = rs.getBoolean("u_disabled");
                    user.nopasswd = rs.getBoolean("u_nopasswd");
                    user.unlockTime = rs.getTimestamp("u_unlock_time").getTime();
                    user.lastSuccessfulLogin = rs.getTimestamp("u_last_successful_login").getTime();
                    user.lastUnsuccessfulLogin = rs.getTimestamp("u_last_unsuccessful_login").getTime();
                    user.consecutiveFailures = rs.getInt("u_consecutive_failures");
                    user.validFrom = rs.getTimestamp("u_valid_from").getTime();
                    user.validTo = rs.getTimestamp("u_valid_to").getTime();

                    user.failedLogins = new TreeSet<>(new Comparator<FailedLogin>() {

                        @Override
                        public int compare(FailedLogin o1, FailedLogin o2) {
                            return Long.compare(o2.minute, o1.minute);
                        }
                    });

                    user.oldPasswords = new TreeSet<>(new Comparator<PasswordHistory>() {
                        @Override
                        public int compare(PasswordHistory o1, PasswordHistory o2) {
                            return Long.compare(o1.date, o2.date);
                        }
                    });
                    do {
                        FailedLogin fl = FailedLogin.fromResultSet(rs);
                        if (fl != null && !user.failedLogins.contains(fl)) {
                            user.failedLogins.add(fl);
                        }

                        PasswordHistory passwordHistory = PasswordHistory.fromResultSet(rs);

                        if (passwordHistory != null) {
                            user.addOldPassword(
                                passwordHistory,
                                context.get(Settings.PASSWORD_HISTORY_LIMIT, Integer.class)
                            );
                        }

                    } while (rs.next());
                }

                return user;
            }
        }

        public static class PasswordHistory {
            public final String password;
            public final long date;

            public PasswordHistory(String password, long date) {
                this.password = password;
                this.date = date;
            }

            public static PasswordHistory fromResultSet(ResultSet rs) throws SQLException{
                PasswordHistory ret = null;
                if (rs.getString("uph_password") != null) {
                    ret = new PasswordHistory(rs.getString("uph_password"), rs.getTimestamp("uph_changed").getTime());
                }
                return ret;
            }
        }

        public static class FailedLogin {

            public final long minute;

            public final int count;

            private FailedLogin(long minute, int count) {
                this.minute = minute;
                this.count = count;
            }

            public static FailedLogin fromResultSet(ResultSet rs) throws SQLException {
                FailedLogin ret = null;
                if (rs.getTimestamp("fl_minute_start") != null) {
                    ret = new FailedLogin(rs.getTimestamp("fl_minute_start").getTime(), rs.getInt("fl_count"));
                }
                return ret;
            }

            @Override
            public String toString() {
                return "FailedLogin{" +
                    "minute=" + minute +
                    ", count=" + count +
                    '}';
            }
        }

        public static final char NOT_ALLOWED = '0';
        private static final int MILLIS_IN_HALF_HOUR = 1000 * 60 * 30;

        private Integer id;
        private String name;
        private String password;
        private long passwordValidTo;
        /**
         * A 336 long string of 0|1 for each half hour during the week.
         * 1 if the user is allowed activity, 0 otherwise.
         * See WEEK_START_SUNDAY.
         */
        private String loginAllowed;
        private boolean disabled;
        private boolean nopasswd;
        private long unlockTime;
        private long lastSuccessfulLogin;
        private long lastUnsuccessfulLogin;
        private int consecutiveFailures;
        private long validFrom;
        private long validTo;

        /**
         * Descending ordered by date.
         */
        private SortedSet<FailedLogin> failedLogins;
        private SortedSet<PasswordHistory> oldPasswords;

        public User() {

        }

        public Integer getId() {
            return id;
        }

        public String getName() {
            return name;
        }

        public String getPassword() {
            return password;
        }

        public long getPasswordValidTo() {
            return passwordValidTo;
        }

        public String getLoginAllowed() {
            return loginAllowed;
        }

        public boolean isNopasswd() {
            return nopasswd;
        }

        public boolean isDisabled() {
            return disabled;
        }

        public long getUnlockTime() {
            return unlockTime;
        }

        public int getConsecutiveFailures() {
            return consecutiveFailures;
        }

        public long getValidTo() {
            return validTo;
        }

        public long getValidFrom() {
            return validFrom;
        }

        public void addOldPassword(PasswordHistory passwordHistory, int passwordHistoryLimit) {
            oldPasswords.add(passwordHistory);
            if (oldPasswords.size() >= passwordHistoryLimit) {
                oldPasswords.remove(oldPasswords.first());
            }
        }

        public List<PasswordHistory> getOldPasswords() {
            return new LinkedList<>(oldPasswords);
        }

        public int countFailuresSince(long date) {
            int count = 0;
            for (FailedLogin next : failedLogins) {
                if (next.minute >= date) {
                    count += next.count;
                } else {
                    break;
                }
            }
            return count;
        }

        @Override
        public String toString() {
            return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", passwordValidTo=" + passwordValidTo +
                ", loginAllowed='" + loginAllowed + '\'' +
                ", disabled=" + disabled + '\'' +
                ", nopasswd=" + nopasswd + '\'' +
                ", unlockTime=" + unlockTime +
                ", lastSuccessfulLogin=" + lastSuccessfulLogin +
                ", lastUnsuccessfulLogin=" + lastUnsuccessfulLogin +
                ", consecutiveFailures=" + consecutiveFailures +
                ", validFrom=" + validFrom +
                ", validTo=" + validTo +
                ", oldPasswords=" + oldPasswords +
                ", failedLogins=" + failedLogins +
                '}';
        }

        public String getWelcomeMessage() {
            return MessageFormat.format(
                "Last successful: {1}, last failed: {2}, consecutive failures: {3}.",
                DateUtils.toISO(lastSuccessfulLogin),
                DateUtils.toISO(lastUnsuccessfulLogin),
                consecutiveFailures
            );

        }

        public String getExpirationMessage(long loginTime, Integer noticeDays) {
            String ret = "";
            if (DateUtils.add(loginTime, Calendar.DAY_OF_MONTH, noticeDays) >  passwordValidTo) {
                ret = Formatter.format("Password will expire at {}.", DateUtils.toISO(passwordValidTo));
            }
            return ret;
        }

        /**
         * Extract date from login allowed string
         *
         * @param loginTime calculate return date base on login at loginTime.
         * @param loginAllowed the string representing a user's time.
         * @return the max date allowed for loginAllowed based on login time.
         *         return loginTime if login is not allowed.
         */
        public static long getLoginAllowed(long loginTime, String loginAllowed) {
            long ret = Long.MAX_VALUE;
            long interval = loginTime - DateUtils.getLastSunday(loginTime);
            int halfHours = (int) interval / MILLIS_IN_HALF_HOUR;
            String startNow = loginAllowed.substring(halfHours) + loginAllowed.substring(0, halfHours);
            int firstDenial = startNow.indexOf(NOT_ALLOWED);
            if (firstDenial != -1) {
                ret = DateUtils.add(loginTime, Calendar.MINUTE, firstDenial * 30);
            }
            return ret;
        }
    }
    private static final Logger LOG = LoggerFactory.getLogger(Schema.class);

    /** Queries */
    private static final String GET_SETTINGS;
    private static final String GET_USER;
    private static final String SEARCH_PRINCIPAL;
    private static final String SEARCH_GROUP;
    private static final String GROUP_MEMBERSHIPS;
    /** SELECT field names for queries exposed for Authz.QueryFilterRecord.KEY */
    public static final Map<ExtKey, String> SEARCH_KEYS = new HashMap<>();
    public static final Map<Integer, String> OPERATORS = new HashMap<>();

    private static final Map<UUID, ExtKey> SETTING_KEYS = new HashMap<>();

    private static final Sql.ResultsResolver<ExtMap> SETTINGS_RESOLVER = new SettingsResolver();
    private static final Sql.ResultsResolver<User> USER_RESOLVER = new User.UserResolver();
    private static final Sql.ResultsResolver<List<ExtMap>> GROUP_MEMBERS_OF_GROUP_RESOLVER = new GroupMembersOfGroupResolver();
    private static final Sql.ResultsResolver<Collection<ExtMap>> ENTITIES_RESOLVER = new searchPageResolver();

    static {
        for (ExtKey key: Arrays.asList(
            Settings.PASSWORD_HISTORY_LIMIT,
            Settings.LOCK_MINUTES,
            Settings.PRESENT_WELCOME_MESSAGE,
            Settings.MESSAGE_SEPARATOR,
            Settings.MESSAGE_OF_THE_DAY,
            Settings.PASSWORD_EXPIRATION_NOTICE_DAYS,
            Settings.PASSWORD_EXPIRATION_DAYS,
            Settings.MAX_FAILURES_PER_MINUTE,
            Settings.MAX_LOGIN_MINUTES,
            Settings.MINIMUM_RESPONSE_SECONDS,
            Settings.MAX_FAILURES_SINCE_SUCCESS,
            Settings.MAX_FAILURES_PER_INTERVAL,
            Settings.INTERVAL_HOURS,
            Settings.ALLOW_EXPIRED_PASSWORD_CHANGE,
            Settings.PASSWORD_COMPLEXITY,
            Settings.PBE_ALGORITHM,
            Settings.PBE_KEY_SIZE,
            Settings.PBE_ITERATIONS,
            Settings.MIN_LENGTH,
            Settings.MAX_PAGE_SIZE ,
            Settings.REFRESH_SETTINGS_INTERVAL_MINUTES,
            Settings.HOUSE_KEEPING_INTERVAL_HOURS,
            Settings.FAILED_LOGINS_OLD_DAYS
        ) ) {
            SETTING_KEYS.put(key.getUuid().getUuid(), key);
        }
        SEARCH_KEYS.put(Authz.PrincipalRecord.NAMESPACE, "namespace");
        SEARCH_KEYS.put(Authz.PrincipalRecord.ID, "u.uuid");
        SEARCH_KEYS.put(Authz.PrincipalRecord.NAME, "u.name");
        SEARCH_KEYS.put(Authz.PrincipalRecord.PRINCIPAL, "u.name");
        SEARCH_KEYS.put(Authz.PrincipalRecord.DISPLAY_NAME, "ua_dn.value");
        SEARCH_KEYS.put(AuthzInternal.USER_DESCRIPTION, "ua_ds.value");
        SEARCH_KEYS.put(Authz.PrincipalRecord.EMAIL, "ua_em.value");
        SEARCH_KEYS.put(Authz.PrincipalRecord.FIRST_NAME, "ua_fn.value");
        SEARCH_KEYS.put(Authz.PrincipalRecord.LAST_NAME, "ua_ln.value");
        SEARCH_KEYS.put(Authz.PrincipalRecord.DEPARTMENT, "ua_dp.value");
        SEARCH_KEYS.put(Authz.PrincipalRecord.TITLE, "ua_tl.value");

        SEARCH_KEYS.put(Authz.GroupRecord.NAMESPACE, "namespace");
        SEARCH_KEYS.put(Authz.GroupRecord.ID, "g.uuid");
        SEARCH_KEYS.put(Authz.GroupRecord.NAME, "g.name");
        SEARCH_KEYS.put(Authz.GroupRecord.DISPLAY_NAME, "ga_dn.value");
        SEARCH_KEYS.put(AuthzInternal.GROUP_DESCRIPTION, "ga_ds.value");

        OPERATORS.put(Authz.QueryFilterOperator.AND, "AND");
        OPERATORS.put(Authz.QueryFilterOperator.OR, "OR");
        OPERATORS.put(Authz.QueryFilterOperator.NOT, "!");
        OPERATORS.put(Authz.QueryFilterOperator.GE, ">=");
        OPERATORS.put(Authz.QueryFilterOperator.LE, "<=");
        OPERATORS.put(Authz.QueryFilterOperator.EQ, "=");
        OPERATORS.put(AuthzInternal.LIKE, "like");


        try (
            InputStream is = Schema.class.getResourceAsStream("sql-statments.properties");
            Reader reader = new InputStreamReader(is, Charset.forName("UTF-8"))
        ) {
            Properties p = new Properties();
            p.load(reader);
            GET_SETTINGS = p.getProperty("catalog.settings.get");
            GET_USER = p.getProperty("authentication.user.get");
            SEARCH_PRINCIPAL = p.getProperty("authorization.principal.search");
            SEARCH_GROUP = p.getProperty("authorization.group.search");
            GROUP_MEMBERSHIPS = p.getProperty("authorization.group.memberships");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static ExtMap get(ExtMap input) throws SQLException {
        ExtMap ret = new ExtMap();
        DataSource ds = input.get(InvokeKeys.DATA_SOURCE, DataSource.class);
        if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.USER)) {
            ret.mput(
                InvokeKeys.USER_RESULT,
                new Sql.Query(
                    Formatter.format(
                        GET_USER,
                        generateWhere(input.get(InvokeKeys.ENTITY_KEYS, ExtMap.class))
                    )
                ).asResults(
                    ds,
                    USER_RESOLVER,
                    input.get(InvokeKeys.SETTINGS_RESULT, ExtMap.class)
                )
            );
        } else if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.SETTINGS)) {
            ret.mput(
                InvokeKeys.SETTINGS_RESULT,
                new Sql.Query(GET_SETTINGS).asResults(
                    ds,
                    SETTINGS_RESOLVER
                )
            );
        } else if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.GROUP_MEMBERSHIPS)) {
            ret.mput(
                InvokeKeys.GROUP_MEMBERSHIPS_RESULT,
                new Sql.Query(
                    Formatter.format(
                        GROUP_MEMBERSHIPS,
                        input.get(InvokeKeys.ENTITY_KEYS, ExtMap.class).get(CursorKeys.FILTER, String.class)
                    )
                ).asResults(
                    ds,
                    GROUP_MEMBERS_OF_GROUP_RESOLVER
                )
            );
        } else if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.CURSOR)) {
            ExtMap searchContext = input.get(Global.InvokeKeys.SEARCH_CONTEXT, ExtMap.class);

            ret.mput(
                InvokeKeys.CURSOR_RESULT,
                new Sql.Query(
                    Formatter.format(
                        searchContext.get(Global.SearchContext.IS_PRINCIPAL, Boolean.class) ?
                        SEARCH_PRINCIPAL :
                        SEARCH_GROUP,
                        input.get(InvokeKeys.ENTITY_KEYS, ExtMap.class).get(CursorKeys.FILTER, String.class)
                    )
                ).asCursor(
                    ds.getConnection(),
                    ENTITIES_RESOLVER,
                    true,
                    searchContext
                )
            );
        } else if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.SEARCH_PAGE)) {
            ret.mput(
                InvokeKeys.SEARCH_PAGE_RESULT,
                input.get(InvokeKeys.CURSOR_RESULT, Sql.Cursor.class).resolve(
                    input.get(Global.InvokeKeys.SEARCH_CONTEXT, ExtMap.class)
                )
            );
        } else {
            throw new UnsupportedOperationException();
        }
        return ret;
    }

    public static void modify(ExtMap input) throws SQLException {
        if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.USER)) {
            modifyUser(
                input.get(InvokeKeys.MODIFICATION_TYPE, Integer.class),
                input.get(InvokeKeys.ENTITY_KEYS, ExtMap.class),
                input.get(InvokeKeys.DATA_SOURCE, DataSource.class)
            );
        } else if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.GROUP)) {
            modifyGroup(
                input.get(InvokeKeys.MODIFICATION_TYPE, Integer.class),
                input.get(InvokeKeys.ENTITY_KEYS, ExtMap.class),
                input.get(InvokeKeys.DATA_SOURCE, DataSource.class)
            );
        } else if (input.get(InvokeKeys.ENTITY, ExtUUID.class).equals(Entities.SETTINGS)) {
            modifySettings(
                input.get(InvokeKeys.ENTITY_KEYS, ExtMap.class),
                input.get(InvokeKeys.DATA_SOURCE, DataSource.class)
            );
        } else {
            throw new RuntimeException();
        }

    }

    private static void modifySettings(ExtMap settingKeys, DataSource dataSource) throws SQLException{
        List<String> updates = new ArrayList<>();
        for (Map.Entry<ExtKey, Object> entry: settingKeys.entrySet()) {
            updates.add(
                Formatter.format(
                    "UPDATE settings SET value = {} WHERE uuid = {}",
                    Formatter.escapeString(entry.getValue()),
                    Formatter.escapeString(entry.getKey()
                            .getUuid()
                            .getUuid()
                    )
                )
            );
        }
        new Sql.Modification(updates).execute(dataSource);
    }

    private static void modifyUser(
        int op,
        ExtMap userKeys,
        DataSource ds
    ) throws SQLException {
        Connection conn = null;
        try {
            Integer id = userKeys.get(UserIdentifiers.USER_ID, Integer.class);
            if (id == null) {
                id = new Sql.Query( // id can never change!
                    Formatter.format(
                        "SELECT id from users where name = {} ",
                        Formatter.escapeString(userKeys.get(UserIdentifiers.USERNAME, String.class))
                    )
                ).asInteger(ds, "id");
                if(id == null && op != Sql.ModificationTypes.INSERT) {
                    throw new EntityNotFoundException("User", userKeys.get(UserIdentifiers.USERNAME, String.class));
                }
                if(id != null && op == Sql.ModificationTypes.INSERT) {
                    throw new EntityAlreadyExists("User", userKeys.get(UserIdentifiers.USERNAME, String.class));
                }
            }

            /**
             *  note: setX() and where() are ignored for deletes and insert accordingly.
             *  @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template
             */
            Sql.Template users = new Sql.Template(op, "users");
            if (userKeys.containsKey(UserKeys.UUID)) {
                users.setString("uuid", userKeys.get(UserKeys.UUID, String.class)
                    .toString());
            } else if (op == Sql.ModificationTypes.INSERT) {
                users.setString("uuid", UUID.randomUUID().toString());
            }
            if (userKeys.containsKey(UserKeys.NEW_USERNAME)) {
                users.setString("name", userKeys.get(UserKeys.NEW_USERNAME, String.class));
            }
            if (userKeys.containsKey(UserKeys.PASSWORD)) { // see InsertPassHistoryRecord
                users.setString("password", userKeys.get(UserKeys.PASSWORD, String.class));
            }
            if (userKeys.containsKey(UserKeys.PASSWORD_VALID_TO)) {
                users.setTimestamp("password_valid_to", userKeys.get(UserKeys.PASSWORD_VALID_TO, Long.class));
            }
            if (userKeys.containsKey(UserKeys.VALID_FROM)) {
                users.setTimestamp("valid_from", userKeys.get(UserKeys.VALID_FROM, Long.class));
            }
            if (userKeys.containsKey(UserKeys.VALID_TO)) {
                users.setTimestamp("valid_to", userKeys.get(UserKeys.VALID_TO, Long.class));
            }
            if (userKeys.containsKey(UserKeys.LOGIN_ALLOWED)) {
                users.setString("login_allowed", userKeys.get(UserKeys.LOGIN_ALLOWED, String.class));
            }
            if (userKeys.containsKey(UserKeys.NOPASS)) {
                users.setBool("nopasswd", userKeys.get(UserKeys.NOPASS, Boolean.class));
            }
            if (userKeys.containsKey(UserKeys.DISABLED)) {
                users.setBool("disabled", userKeys.get(UserKeys.DISABLED, Boolean.class));
            }
            if (userKeys.containsKey(UserKeys.UNLOCK_TIME)) {
                users.setTimestamp("unlock_time", userKeys.get(UserKeys.UNLOCK_TIME, Long.class));
            }
            if (userKeys.containsKey(UserKeys.SUCCESSFUL_LOGIN)) {
                users.setTimestamp("last_successful_login", userKeys.get(UserKeys.SUCCESSFUL_LOGIN, Long.class));
            }
            if (userKeys.containsKey(UserKeys.UNLOCK_TIME) ||
                    userKeys.containsKey(UserKeys.SUCCESSFUL_LOGIN)) {
                users.setInteger("consecutive_failures", 0);
            }
            if (userKeys.containsKey(UserKeys.UNSUCCESSFUL_LOGIN)) {
                users.setTimestamp("last_unsuccessful_login", userKeys.get(UserKeys.UNSUCCESSFUL_LOGIN, Long.class));
                if (op != Sql.ModificationTypes.INSERT) {
                    users.setRaw("consecutive_failures", "(consecutive_failures + 1)");
                }
            }
            users.setInteger("id", id);

            conn = ds.getConnection();
            ExtMap ctx = new Sql.Modification( // execute "users" statement
               users.where(
                   generateWhere(userKeys)
               ).asSql()
            ).execute(conn, false);


            if (ctx.get(Sql.ModificationContext.LAST_STATEMENT_ROWS, Integer.class) <= 0) {
                throw new SQLException("ERROR: No rows effected\n");
            }

            if (op == Sql.ModificationTypes.INSERT) {
                id = (Integer) ctx.get(Sql.ModificationContext.GENERATED_IDS, List/**<Integer>*/.class).get(0);
            }
            if (op != Sql.ModificationTypes.DELETE && userKeys.containsKey(SharedKeys.ATTRIBUTES)) { // on delete cascade
                upsertAttribute(id, userKeys.<Collection<ExtMap>>get(SharedKeys.ATTRIBUTES), conn, true);
            }
            if (
                op == Sql.ModificationTypes.UPDATE &&
                userKeys.containsKey(UserKeys.PASSWORD) &&
                !StringUtils.isEmpty(userKeys.get(UserKeys.OLD_PASSWORD, String.class))
            ) {
                InsertPassHistoryRecord(id, userKeys, conn);
            }
            if (op == Sql.ModificationTypes.UPDATE && userKeys.containsKey(UserKeys.UNSUCCESSFUL_LOGIN)) {
                upsertFailedLoginRecord(id, userKeys, conn);
            }
            if (op == Sql.ModificationTypes.UPDATE && userKeys.containsKey(SharedKeys.ADD_GROUP)) {
                updateGroupMembership(id, userKeys.get(SharedKeys.ADD_GROUP, String.class), conn, true, true);
            }
            if (op == Sql.ModificationTypes.UPDATE && userKeys.containsKey(SharedKeys.REMOVE_GROUP)) {
                updateGroupMembership(id, userKeys.get(SharedKeys.REMOVE_GROUP, String.class), conn, false, true);
            }

            conn.commit();

        } catch (SQLException e) {
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (Throwable thr) {
                    LOG.warn("Could not roll back connection, ignoring", thr);
                }
            }
            throw e;
        } finally {
            Sql.closeQuietly(conn);
        }
    }

    private static void modifyGroup(int op, ExtMap groupKeys, DataSource ds) throws SQLException {
        /**
         *  note: setX() and where() are ignored for deletes and insert accordingly.
         *  @see org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql.Template
         */
        Connection conn = null;
        try {
            Integer id = new Sql.Query( // id can never change!
                Formatter.format(
                    "SELECT id from groups where name = {} ",
                    Formatter.escapeString(groupKeys.get(GroupIdentifiers.NAME, String.class))
                )
            ).asInteger(ds, "id");
            if (id == null && op != Sql.ModificationTypes.INSERT) {
                throw new EntityNotFoundException("Group", groupKeys.get(GroupIdentifiers.NAME, String.class));
            }
            if(id != null && op == Sql.ModificationTypes.INSERT) {
                throw new EntityAlreadyExists("Group", groupKeys.get(GroupIdentifiers.NAME, String.class));
            }


            Sql.Template group = new Sql.Template(op, "groups");
            if (groupKeys.containsKey(GroupKeys.UUID)) {
                group.setString("uuid", groupKeys.get(GroupKeys.UUID, String.class));
            } else if (op == Sql.ModificationTypes.INSERT) {
                group.setString("uuid", UUID.randomUUID().toString());
            }
            if (groupKeys.containsKey(GroupKeys.NEW_NAME) && op == Sql.ModificationTypes.UPDATE) {
                group.setString("name", groupKeys.get(GroupKeys.NEW_NAME, String.class));
            } else {
                group.setString("name", groupKeys.get(GroupIdentifiers.NAME, String.class));
            }
            conn = ds.getConnection();
            ExtMap ctx = new Sql.Modification( // execute "groups" statement
                group.where(
                    Formatter.format(
                        "name = {}",
                        Formatter.escapeString(groupKeys.get(GroupIdentifiers.NAME, String.class))
                    )
                ).asSql()
            ).execute(conn, false);

            if (ctx.get(Sql.ModificationContext.LAST_STATEMENT_ROWS, Integer.class) <= 0) {
                throw new SQLException("ERROR: No rows effected\n");
            }
            if (op == Sql.ModificationTypes.INSERT) {
                id = (Integer) ctx.get(Sql.ModificationContext.GENERATED_IDS, List/**<Integer>*/.class).get(0);
            }
            if (op != Sql.ModificationTypes.DELETE && groupKeys.containsKey(SharedKeys.ATTRIBUTES)) { // on delete cascade

                upsertAttribute(id, groupKeys.<Collection<ExtMap>>get(SharedKeys.ATTRIBUTES), conn, false);
            }
            if (op == Sql.ModificationTypes.UPDATE && groupKeys.containsKey(SharedKeys.ADD_GROUP)) {
                updateGroupMembership(id, groupKeys.get(SharedKeys.ADD_GROUP, String.class), conn, true, false);
            }
            if (op == Sql.ModificationTypes.UPDATE && groupKeys.containsKey(SharedKeys.REMOVE_GROUP)) {
                updateGroupMembership(id, groupKeys.get(SharedKeys.REMOVE_GROUP, String.class), conn, false, false);
            }
            conn.commit();
        } catch (SQLException e) {
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (Throwable thr) {
                    LOG.warn("Could not roll back connection", e);
                }
            }
            throw e;
        } finally {
            Sql.closeQuietly(conn);
        }
    }

    private static void updateGroupMembership(
        int memberId,
        String groupName,
        Connection conn,
        boolean add,
        boolean principal
    ) throws SQLException {
        String memberCol = principal ? "user_id" : "group_id";
        Integer groupId = new Sql.Query( // id's can never change
            Formatter.format(
                "SELECT id from groups where name = {}",
                Formatter.escapeString(groupName))
        ).asInteger(conn, "id");
        if (groupId == null) {
            throw new EntityNotFoundException("group",  groupName);
        }

        new Sql.Modification(
            new Sql.Template(
                add?
                Sql.ModificationTypes.INSERT:
                Sql.ModificationTypes.DELETE,
                principal?
                "user_groups":
                "group_groups"
            ).setInteger(
                memberCol,
                memberId
            ).setInteger(
                "in_group_id",
                groupId
            ).where(
                Formatter.format("{} = {} AND {} = {}", memberCol, memberId, "in_group_id", groupId)
            ).asSql()
        ).execute(conn, false);
    }

    private static String generateWhere(ExtMap input) {
        String cond;
        if (input.get(UserIdentifiers.USER_ID) != null) {
            cond =
                Formatter.format(
                    "id = {}",
                    input.<Integer>get(UserIdentifiers.USER_ID)
                );
        } else if (input.get(UserIdentifiers.USERNAME) != null) {
            cond =
                Formatter.format(
                    "name = {}",
                    Formatter.escapeString(input.<String>get(UserIdentifiers.USERNAME, String.class))
                );
        } else {
            throw new RuntimeException("No user Identifier provided");
        }
        return cond;
    }

    private static void upsertFailedLoginRecord(Integer id, ExtMap input, Connection conn)
        throws SQLException{
        long minuteStart = input.get(UserKeys.UNSUCCESSFUL_LOGIN, Long.class);
        minuteStart -= (minuteStart % (1000 * 60));
        if (
            new Sql.Query(
                Formatter.format(
                    "SELECT count FROM failed_logins WHERE user_id = {} and minute_start = {}",
                    id,
                    DateUtils.toTimestamp(minuteStart)
                )
            ).asInteger(conn, "count") == null
        ) {
            new Sql.Modification(
                new Sql.Template(Sql.ModificationTypes.INSERT, "failed_logins")
                .setInteger("user_id", id)
                .setTimestamp("minute_start", minuteStart)
                .setInteger("count", 1)
                .asSql()
            ).execute(conn, false);
        } else {
            new Sql.Modification(
                new Sql.Template(Sql.ModificationTypes.UPDATE, "failed_logins")
                .setIncrement("count")
                .where(
                    Formatter.format(
                        "user_id = {} AND minute_start = {}",
                        id,
                        DateUtils.toTimestamp(minuteStart)
                    )
                ).asSql()
            ).execute(conn, false);
        }
    }

    private static void InsertPassHistoryRecord(Integer id, ExtMap input, Connection conn)
        throws SQLException {
        new Sql.Modification(
            new Sql.Template(Sql.ModificationTypes.INSERT, "user_password_history")
            .setInteger("user_id", id)
            .setString("password", input.get(UserKeys.OLD_PASSWORD, String.class))
            .setTimestamp("changed", System.currentTimeMillis())
            .asSql()
        ).execute(conn, false);
    }

    private static void upsertAttribute(int id, Collection<ExtMap> attributes, Connection conn, boolean user)
        throws SQLException {
        for (ExtMap attribute: attributes) {
            if (hasAttribute(id, conn, attribute, user)) {
                new Sql.Modification(
                    new Sql.Template(
                        Sql.ModificationTypes.UPDATE,
                        user? "user_attributes": "group_attributes"
                    )
                    .setString("value", attribute.get(SharedKeys.ATTRIBUTE_VALUE, String.class))
                    .where(
                        Formatter.format(
                            "{} = {} AND name = {}",
                            user ? "user_id" : "group_id",
                            id,
                            Formatter.escapeString(attribute.get(SharedKeys.ATTRIBUTE_NAME, String.class))
                        )
                    ).asSql()
                ).execute(conn, false);
            } else {
                new Sql.Modification(
                    new Sql.Template(
                        Sql.ModificationTypes.INSERT,
                        user? "user_attributes": "group_attributes") // always insert first
                        .setString("name", attribute.get(SharedKeys.ATTRIBUTE_NAME, String.class))
                        .setString("value", attribute.get(SharedKeys.ATTRIBUTE_VALUE, String.class))
                        .setInteger(user? "user_id": "group_id", id)
                        .where(Formatter.format(user ? "user_id" : "group_id", id))
                        .asSql()
                ).execute(conn, false); // note: executed in new connection and committed
            }
        }
    }

    private static boolean hasAttribute(int id, Connection conn, ExtMap attribute, boolean user) throws SQLException {
        return new Sql.Query(
            Formatter.format(
                "SELECT value FROM {} WHERE {} = {} and name = {}",
                user ? "user_attributes" : "group_attributes",
                user ? "user_id" : "group_id",
                id,
                Formatter.escapeString(attribute.get(SharedKeys.ATTRIBUTE_NAME, String.class))
            )
        ).asString(conn, "value") != null;
    }

    public static class EntityNotFoundException extends SQLException {
        private static final long serialVersionUID = 6612935575387993623L;

        public EntityNotFoundException(String what, String who) {
            super(new StringBuilder(what).append(" ").append(who).append(" ").append("not found").toString());
        }
    }

    public static class EntityAlreadyExists extends SQLException {
        private static final long serialVersionUID = -1077934189115100475L;

        public EntityAlreadyExists(String what, String who) {
            super(new StringBuilder(what).append(" ").append(who).append(" ").append("already exists").toString());
        }
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/core/Authorization.java
public class Authorization implements Observer {
    private ExtMap settings;
    private final DataSource ds;
    private AtomicLong nextOpaque = new AtomicLong(1);
    private Map<String, Sql.Cursor<Collection<ExtMap>>> openSelects = new HashMap<>();
    private Map<String, ExtMap> searchContexts = new HashMap<>();

    public Authorization(DataSource ds) {
        this.ds = ds;
    }

    @SuppressWarnings("unchecked")
    public String openQuery(
        String filter,
        ExtMap searchContext
    ) throws SQLException {
        String opaque = Long.toString(nextOpaque.getAndIncrement());

        searchContexts.put(opaque, searchContext);
        openSelects.put(
            opaque,
            Schema.get(
                new ExtMap().mput(Schema.InvokeKeys.ENTITY, Schema.Entities.CURSOR)
                    .mput(
                        Schema.InvokeKeys.ENTITY_KEYS, new ExtMap().mput(Schema.CursorKeys.FILTER, filter)
                    ).mput(Global.InvokeKeys.SEARCH_CONTEXT, searchContext)
                    .mput(Schema.InvokeKeys.DATA_SOURCE, ds)
            ).get(
                Schema.InvokeKeys.CURSOR_RESULT,
                Sql.Cursor.class
            )
        );
        return opaque;
    }

    @SuppressWarnings("unchecked")
    public Collection<ExtMap> executeQuery(String opaque, Integer pageSize) throws SQLException, IOException {
        Collection<ExtMap> results = Schema.get(
            new ExtMap().mput(Schema.InvokeKeys.ENTITY, Schema.Entities.SEARCH_PAGE)
            .mput(Schema.InvokeKeys.CURSOR_RESULT, openSelects.get(opaque))
            .mput(Global.InvokeKeys.SEARCH_CONTEXT, new ExtMap().mput(
                Global.SearchContext.PAGE_SIZE,
                Math.min(settings.get(Schema.Settings.MAX_PAGE_SIZE, Integer.class), pageSize))
            )
        ).get(
            Schema.InvokeKeys.SEARCH_PAGE_RESULT,
            Collection.class
        );
        if (results != null && searchContexts.get(opaque).<Boolean>get(Global.SearchContext.RECURSIVE)) {
            for (ExtMap result : results) {
                if (result != null) {
                    if (searchContexts.get(opaque).get(Global.SearchContext.IS_PRINCIPAL)) {
                        resolveGroupsMemberships(
                                result.<List<ExtMap>>get(Authz.PrincipalRecord.GROUPS),
                                new HashMap<String, ExtMap>()
                        );
                    } else {
                        resolveGroupsMemberships(
                                Arrays.asList(result),
                                new HashMap<String, ExtMap>()
                        );
                    }
                }
            }
        }
        return results;
    }

    public void closeQuery(String opaque) {
        Sql.Cursor<Collection<ExtMap>> remove = openSelects.remove(opaque);
        remove.close();
        searchContexts.remove(opaque);
    }

    public Collection<ExtMap> getResults(String filter, ExtMap searchContext) throws SQLException, IOException {
        String opaque = null;
        Collection<ExtMap> principals;
        try {
            opaque = openQuery(filter, searchContext);
            principals = executeQuery(
                opaque,
                searchContext.get(Global.SearchContext.PAGE_SIZE, Integer.class)
            );
            return principals;
        } finally {
            if (opaque!= null) {
                closeQuery(opaque);
            }
        }
    }

    @Override
    public void update(Observable o, Object arg) {
        this.settings = (ExtMap)arg;
    }

    private void resolveGroupsMemberships(List<ExtMap> groupsToResolve, Map<String, ExtMap> resolvedGroups)
            throws IOException, SQLException {
        if (groupsToResolve != null) {
            for (ExtMap groupRecord : groupsToResolve) {
                if (resolvedGroups.containsKey(groupRecord.<String>get(Authz.GroupRecord.ID))) {
                    groupRecord.put(
                            Authz.GroupRecord.GROUPS,
                            resolvedGroups.get(groupRecord.<String>get(Authz.GroupRecord.ID))
                                    .get(Authz.GroupRecord.GROUPS));
                } else {
                    resolvedGroups.put(groupRecord.<String>get(Authz.GroupRecord.ID), groupRecord);
                    groupRecord.put(Authz.GroupRecord.GROUPS, getGroupMemberships(groupRecord));
                    resolveGroupsMemberships(
                            groupRecord.<List<ExtMap>>get(Authz.GroupRecord.GROUPS),
                            resolvedGroups);
                }
            }
        }
    }

    private List<ExtMap> getGroupMemberships(ExtMap groupRecord) throws IOException, SQLException {
        return Schema.get(
            new ExtMap().mput(
                Schema.InvokeKeys.ENTITY, Schema.Entities.GROUP_MEMBERSHIPS
            ).mput(
                Schema.InvokeKeys.ENTITY_KEYS,
                new ExtMap().mput(
                    Schema.CursorKeys.FILTER,
                    Formatter.format(
                        "{} = {}",
                        Schema.SEARCH_KEYS.get(Authz.GroupRecord.ID),
                        Formatter.escapeString(groupRecord.get(Authz.GroupRecord.ID))
                    )
                )
            ).mput(
                Global.InvokeKeys.SEARCH_CONTEXT,
                new ExtMap().mput(
                    Global.SearchContext.ALL_ATTRIBUTES,
                    true
                )
            ).mput(
                Schema.InvokeKeys.DATA_SOURCE,
                ds
            )
        ).get(Schema.InvokeKeys.GROUP_MEMBERSHIPS_RESULT);
    }
}
src/main/java/org/ovirt/engine/extension/aaa/jdbc/binding/Config.java
public class Config {
    public static final String PACKAGE_NAME;
    public static final String PACKAGE_VERSION;
    public static final String PACKAGE_DISPLAY_NAME;

    static {
        try (
            InputStream is = Config.class.getResourceAsStream("package.properties");
            Reader reader = new InputStreamReader(is, Charset.forName("UTF-8"))
        ) {
            Properties p = new Properties();
            p.load(reader);
            PACKAGE_NAME = p.getProperty("config.package.name");
            PACKAGE_VERSION = p.getProperty("config.package.version");
            PACKAGE_DISPLAY_NAME = p.getProperty("config.package.displayName");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}


Question: import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.api.extensions.ExtKey;
import org.ovirt.engine.api.extensions.ExtMap;
import org.ovirt.engine.api.extensions.ExtUUID;
import org.ovirt.engine.api.extensions.aaa.Authn;
import org.ovirt.engine.api.extensions.aaa.Authz;
import org.ovirt.engine.extension.aaa.jdbc.DateUtils;
import org.ovirt.engine.extension.aaa.jdbc.Formatter;
import org.ovirt.engine.extension.aaa.jdbc.Global;
import org.ovirt.engine.extension.aaa.jdbc.binding.Config;
import org.ovirt.engine.extension.aaa.jdbc.binding.api.ExtensionUtils;
import org.ovirt.engine.extension.aaa.jdbc.binding.cli.command.Command;
import org.ovirt.engine.extension.aaa.jdbc.binding.cli.command.GroupManageShowCommand;
import org.ovirt.engine.extension.aaa.jdbc.core.Authentication;
import org.ovirt.engine.extension.aaa.jdbc.core.Authorization;
import org.ovirt.engine.extension.aaa.jdbc.core.EnvelopePBE;
import org.ovirt.engine.extension.aaa.jdbc.core.Schema;
import org.ovirt.engine.extension.aaa.jdbc.core.datasource.DataSourceProvider;
import org.ovirt.engine.extension.aaa.jdbc.core.datasource.Sql;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
                        .append(Formatter.escapeString(val)).append(" AND ");
                    }
                    filter.setLength(filter.length() - 5);
                    context.mput(
                        ContextKeys.SEARCH_FILTER,
                        filter.toString()
                    ).mput(
                        Global.InvokeKeys.SEARCH_CONTEXT,
                        new ExtMap().mput(Global.SearchContext.PAGE_SIZE, 100)
                        .mput(Global.SearchContext.IS_PRINCIPAL, isPrincipal)
                        .mput(Global.SearchContext.RECURSIVE, false)
                        .mput(Global.SearchContext.WITH_GROUPS, true)
                        .mput(Global.SearchContext.ALL_ATTRIBUTES, true)
                    );

                    commands.get("_search").invoke(context);

                    if (!context.containsKey(ContextKeys.EXIT_STATUS)) {
                        commands.get("_show").invoke(context);
                    }
                }
            },
            /**
             * Following are internal methods that cannot be invoked from command line.
             */
            new Command() {
                @Override
                public String getName() {
                    return "_schema-get";
                }

                @SuppressWarnings("unchecked")
                @Override
                public void invoke(ExtMap context, Map<String, Object> args) {
                    try {
                        context.putAll(
                            Schema.get(
                                new ExtMap().mput(Schema.InvokeKeys.DATA_SOURCE, context.get(Schema.InvokeKeys.DATA_SOURCE))
                                .mput(Schema.InvokeKeys.ENTITY, context.get(Schema.InvokeKeys.ENTITY))
                                .mput(Schema.InvokeKeys.ENTITY_KEYS, context.get(Schema.InvokeKeys.ENTITY_KEYS))
                                .mput(Schema.InvokeKeys.SETTINGS_RESULT, context.get(Schema.InvokeKeys.SETTINGS_RESULT))
                                .mput(Schema.InvokeKeys.USER_RESULT, context.get(Schema.InvokeKeys.USER_RESULT))
                            )
                        );
                    } catch (SQLException e) {
                        context.put(ContextKeys.EXIT_STATUS, SQL_ERROR);
                        addContextMessage(context, true, e.getMessage());

                        context.<List<Throwable>>get(ContextKeys.THROWABLES).add(e);
                    }
                }
            },
            new Command() {
                @Override
                public String getName() {
                    return "_schema-modify";
                }

                @SuppressWarnings("unchecked")
                @Override
                public void invoke(ExtMap context, Map<String, Object> args) {
                    try {
                        Integer modType = context.get(Schema.InvokeKeys.MODIFICATION_TYPE, Integer.class);
                        String modName = "add";
                        if (modType == Sql.ModificationTypes.DELETE) {
                            modName = "delet";
                        } else if (modType == Sql.ModificationTypes.UPDATE) {
                            modName = "updat";
                        }
                        System.out.print(
                            Formatter.format(
                                "{}ing {} {}...\n",
                                modName,
                                ENTITY_NAMES.get(context.get(Schema.InvokeKeys.ENTITY, ExtUUID.class)),
                                context.<String>get(ContextKeys.POSITIONAL) != null ? // settings have no name
                                context.<String>get(ContextKeys.POSITIONAL) :
                                ""
                            ));
                        ExtMap modification =
                            new ExtMap().mput(Schema.InvokeKeys.ENTITY, context.get(Schema.InvokeKeys.ENTITY))
                            .mput(Schema.InvokeKeys.ENTITY_KEYS, context.get(Schema.InvokeKeys.ENTITY_KEYS))
                            .mput(Schema.InvokeKeys.USER_RESULT, context.get(Schema.InvokeKeys.USER_RESULT))
                            .mput(Schema.InvokeKeys.MODIFICATION_TYPE, modType) // removing
                            .mput(Schema.InvokeKeys.DATA_SOURCE, context.get(Schema.InvokeKeys.DATA_SOURCE));
                        LOG.trace(Formatter.format("executing modification: {}", modification));
                        Schema.modify(modification);
                        System.out.print(
                            Formatter.format(
                                "{} {}ed successfully\n",
                                ENTITY_NAMES.get(context.get(Schema.InvokeKeys.ENTITY, ExtUUID.class)),
                                modName
                            )
                        );
                    } catch (Schema.EntityNotFoundException e) {
                        addContextMessage(context, true, e.getMessage());
                        context.<List<Throwable>>get(ContextKeys.THROWABLES).add(e);
                        context.put(ContextKeys.EXIT_STATUS, NOT_FOUND);
                    } catch (Schema.EntityAlreadyExists e) {
                        addContextMessage(context, true, e.getMessage());
                        context.<List<Throwable>>get(ContextKeys.THROWABLES).add(e);
                        context.put(ContextKeys.EXIT_STATUS, ALREADY_EXISTS);
                    } catch (SQLException e) {
                        addContextMessage(context, true, e.getMessage());
                        context.<List<Throwable>>get(ContextKeys.THROWABLES).add(e);
                        context.put(ContextKeys.EXIT_STATUS, SQL_ERROR);
                    }
                }
            },
            new Command() {
                @Override
                public String getName() {
                    return "_search";
                }


                @SuppressWarnings("unchecked")
                @Override
                public  void invoke(ExtMap context, Map<String, Object> args) {
                    Collection<ExtMap> res = null;
                    try {
Answer:                         Authorization authorization = new Authorization(context.get(Schema.InvokeKeys.DATA_SOURCE, DataSource.class));