diff --git a/com.io7m.idstore.database.postgres/src/main/java/com/io7m/idstore/database/postgres/internal/IdDatabaseExceptions.java b/com.io7m.idstore.database.postgres/src/main/java/com/io7m/idstore/database/postgres/internal/IdDatabaseExceptions.java index 60677460..3bfd8600 100644 --- a/com.io7m.idstore.database.postgres/src/main/java/com/io7m/idstore/database/postgres/internal/IdDatabaseExceptions.java +++ b/com.io7m.idstore.database.postgres/src/main/java/com/io7m/idstore/database/postgres/internal/IdDatabaseExceptions.java @@ -125,7 +125,7 @@ yield new IdDatabaseException( Objects.requireNonNullElse(serverError.getConstraint(), ""); yield switch (constraint) { - case "users_id_name_key" -> { + case "users_id_name_index" -> { yield new IdDatabaseException( "User ID name already exists", USER_DUPLICATE_ID_NAME, diff --git a/com.io7m.idstore.database.postgres/src/main/resources/com/io7m/idstore/database/postgres/internal/database.xml b/com.io7m.idstore.database.postgres/src/main/resources/com/io7m/idstore/database/postgres/internal/database.xml index 4b7cb1d1..a615d87b 100644 --- a/com.io7m.idstore.database.postgres/src/main/resources/com/io7m/idstore/database/postgres/internal/database.xml +++ b/com.io7m.idstore.database.postgres/src/main/resources/com/io7m/idstore/database/postgres/internal/database.xml @@ -9,10 +9,10 @@ @@ -21,10 +21,10 @@ create role idstore with nosuperuser nocreatedb nocreaterole noinherit nologin; @@ -35,13 +35,16 @@ create role idstore_none with nosuperuser nocreatedb nocreaterole noinherit nolo @@ -52,69 +55,72 @@ create table schema_version ( - grant select, insert on user_ids to idstore + GRANT SELECT, INSERT, DELETE ON user_ids TO idstore The admins table stores the current set of admins. - grant select, insert, update, delete on admins to idstore + + GRANT SELECT, INSERT, UPDATE, DELETE ON admins TO idstore + + + GRANT SELECT (id) ON admins TO idstore_none - grant select (id) on admins to idstore_none = 1 then - raise sqlstate 'ID002' using message = 'Only one admin can be the initial admin.'; - return null; - end if; - end if; + IF old.initial = false and new.initial = true THEN + SELECT count(*) into count FROM admins a WHERE a.initial = true; + IF count >= 1 THEN + RAISE sqlstate 'ID002' USING message = 'Only one admin can be the initial admin.'; + RETURN NULL; + END IF; + END IF; return old; end; $$ language plpgsql; ]]> @@ -122,25 +128,34 @@ create trigger admin_initial_check_update - grant select, insert, update, delete on users to idstore + + GRANT SELECT, INSERT, UPDATE, DELETE ON users TO idstore + + + GRANT SELECT (id) ON users TO idstore_none - grant select (id) on users to idstore_none + + The emails table stores the set of email addresses used by users/admins. @@ -150,66 +165,66 @@ create table users ( - grant select, insert, delete on emails to idstore + GRANT SELECT, INSERT, DELETE ON emails TO idstore - The email_check_delete trigger enforces the constraint that users must + The email_check_delete trigger enforces the CONSTRAINT that users must have at least one email address at any time. @@ -218,37 +233,36 @@ create trigger email_check_delete_trigger - grant insert, select on audit to idstore + GRANT INSERT, SELECT ON audit TO idstore The login_history table stores a history of user/admin logins. - grant insert, select, delete on login_history to idstore - + GRANT INSERT, SELECT, DELETE ON login_history TO idstore The email_verifications table records the email verification operations in @@ -258,20 +272,20 @@ create table login_history ( token_deny) +CREATE TABLE email_verifications ( + user_id UUID NOT NULL, + email VARCHAR(1000000) NOT NULL, + token_permit VARCHAR(1000000) NOT NULL unique, + token_deny VARCHAR(1000000) NOT NULL unique, + expires TIMESTAMP WITH TIME ZONE NOT NULL, + operation VARCHAR(1000000) NOT NULL, + + FOREIGN KEY (user_id) REFERENCES user_ids (id), + CONSTRAINT tokens_not_equal CHECK (token_permit <> token_deny) ) ]]> - grant insert, select, delete on email_verifications to idstore + GRANT INSERT, SELECT, DELETE ON email_verifications TO idstore @@ -280,16 +294,16 @@ create table email_verifications ( - grant insert, select, update, delete on bans to idstore + GRANT INSERT, SELECT, UPDATE, DELETE ON bans TO idstore @@ -299,16 +313,16 @@ create table bans ( - grant insert, select, delete on user_password_resets to idstore + GRANT INSERT, SELECT, DELETE ON user_password_resets TO idstore @@ -320,25 +334,40 @@ create table user_password_resets ( - grant select on admins to idstore_read_only - grant select on audit to idstore_read_only - grant select on bans to idstore_read_only - grant select on email_verifications to idstore_read_only + GRANT SELECT ON admins TO idstore_read_only + + + GRANT SELECT ON audit TO idstore_read_only + + + GRANT SELECT ON bans TO idstore_read_only + + + GRANT SELECT ON email_verifications TO idstore_read_only + + + GRANT SELECT ON emails TO idstore_read_only + + + GRANT SELECT ON login_history TO idstore_read_only + + + GRANT SELECT ON schema_version TO idstore_read_only + + + GRANT SELECT ON user_ids TO idstore_read_only - grant select on emails to idstore_read_only - grant select on login_history to idstore_read_only - grant select on schema_version to idstore_read_only - grant select on user_ids to idstore_read_only - grant select on user_password_resets to idstore_read_only + GRANT SELECT ON user_password_resets TO idstore_read_only + + GRANT SELECT ON users TO idstore_read_only - grant select on users to idstore_read_only diff --git a/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/model.xml b/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/model.xml index 3ada7c87..3453ab1c 100644 --- a/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/model.xml +++ b/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/model.xml @@ -45,11 +45,14 @@ User - and administrator accounts have names that are used for login operations. Names must - match the regular expression - \p{LC}[\p{LC}\p{N}_-]{0,255}. Names must be unique within a given - idstore - server, but can be changed at the user/administrator's discretion at any time. + and administrator accounts have names that are + used for login operations. Names must match the regular expression + \p{LC}[\p{LC}\p{N}_-]{0,255}. Names + are case-insensitive + and must be unique within a given + idstore server, but can be changed at the + user/administrator's discretion at any time. @@ -325,4 +328,10 @@ + + Names are case-insensitive in order to make it slightly more difficult for + a hostile user to impersonate another by picking a name that only varies + with case. + + diff --git a/com.io7m.idstore.model/src/main/java/com/io7m/idstore/model/IdName.java b/com.io7m.idstore.model/src/main/java/com/io7m/idstore/model/IdName.java index 244fb3d0..d759358a 100644 --- a/com.io7m.idstore.model/src/main/java/com/io7m/idstore/model/IdName.java +++ b/com.io7m.idstore.model/src/main/java/com/io7m/idstore/model/IdName.java @@ -16,7 +16,7 @@ package com.io7m.idstore.model; -import java.util.Comparator; +import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; @@ -54,6 +54,25 @@ public record IdName(String value) } } + @Override + public int hashCode() + { + return this.value.toUpperCase(Locale.ROOT).hashCode(); + } + + @Override + public boolean equals( + final Object obj) + { + if (obj == null) { + return false; + } + if (obj instanceof final IdName other) { + return this.value.equalsIgnoreCase(other.value); + } + return false; + } + @Override public String toString() { @@ -64,7 +83,11 @@ public String toString() public int compareTo( final IdName other) { - return Comparator.comparing(IdName::value) - .compare(this, other); + final var thisV = + this.value.toUpperCase(Locale.ROOT); + final var thatV = + other.value.toUpperCase(Locale.ROOT); + + return thisV.compareTo(thatV); } } diff --git a/com.io7m.idstore.tests/src/main/java/com/io7m/idstore/tests/database/IdDatabaseUsersTest.java b/com.io7m.idstore.tests/src/main/java/com/io7m/idstore/tests/database/IdDatabaseUsersTest.java index 7ff002cd..dcceb26f 100644 --- a/com.io7m.idstore.tests/src/main/java/com/io7m/idstore/tests/database/IdDatabaseUsersTest.java +++ b/com.io7m.idstore.tests/src/main/java/com/io7m/idstore/tests/database/IdDatabaseUsersTest.java @@ -306,6 +306,21 @@ public void testUserDuplicateId() assertEquals(USER_DUPLICATE_ID_NAME, ex.errorCode()); } + { + final var ex = + assertThrows(IdDatabaseException.class, () -> { + users.userCreate( + randomUUID(), + user.idName(), + new IdRealName("SOMEONE"), + new IdEmail("someone2@example.com"), + now, + password + ); + }); + assertEquals(USER_DUPLICATE_ID_NAME, ex.errorCode()); + } + { final var ex = assertThrows(IdDatabaseException.class, () -> {