Ensure Auth entity has a unique username (#1257)

This commit is contained in:
Filip Sodić 2023-06-16 17:01:55 +02:00 committed by GitHub
parent c5540ae742
commit 90bbff3b64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 29 deletions

View File

@ -1,6 +1,6 @@
app crudTesting {
wasp: {
version: "^0.10.4"
version: "^0.11.0"
},
head: [
"<link rel=\"stylesheet\" href=\"https://unpkg.com/mvp.css@1.12/mvp.css\">"

View File

@ -7,15 +7,19 @@ module Wasp.AppSpec.Entity
getPslModelBody,
getIdField,
getIdBlockAttribute,
isFieldUnique,
-- only for testing:
doesFieldHaveAttribute,
)
where
import Data.Data (Data)
import Data.List (find)
import Wasp.AppSpec.Core.Decl (IsDecl)
import Wasp.AppSpec.Entity.Field (Field)
import qualified Wasp.AppSpec.Entity.Field as Field
import qualified Wasp.Psl.Ast.Model as PslModel
import Wasp.Psl.Util (findIdBlockAttribute, findIdField)
import Wasp.Psl.Util (doesPslFieldHaveAttribute, findIdBlockAttribute, findIdField)
data Entity = Entity
{ fields :: ![Field],
@ -45,5 +49,18 @@ getPslModelBody = pslModelBody
getIdField :: Entity -> Maybe PslModel.Field
getIdField = findIdField . getPslModelBody
isFieldUnique :: String -> Entity -> Maybe Bool
isFieldUnique fieldName = doesFieldHaveAttribute fieldName "unique"
doesFieldHaveAttribute :: String -> String -> Entity -> Maybe Bool
doesFieldHaveAttribute fieldName attrName entity =
doesPslFieldHaveAttribute attrName <$> findPslFieldByName fieldName entity
findPslFieldByName :: String -> Entity -> Maybe PslModel.Field
findPslFieldByName fieldName Entity {pslModelBody = PslModel.Body elements} =
find isField [field | (PslModel.ElementField field) <- elements]
where
isField PslModel.Field {_name = name} = name == fieldName
getIdBlockAttribute :: Entity -> Maybe PslModel.Attribute
getIdBlockAttribute = findIdBlockAttribute . getPslModelBody

View File

@ -27,6 +27,7 @@ import qualified Wasp.AppSpec.App.Db as AS.Db
import qualified Wasp.AppSpec.App.Wasp as Wasp
import Wasp.AppSpec.Core.Decl (takeDecls)
import qualified Wasp.AppSpec.Crud as AS.Crud
import Wasp.AppSpec.Entity (isFieldUnique)
import qualified Wasp.AppSpec.Entity as Entity
import qualified Wasp.AppSpec.Entity.Field as Entity.Field
import qualified Wasp.AppSpec.Page as Page
@ -149,14 +150,37 @@ validateAuthUserEntityHasCorrectFieldsIfUsernameAndPasswordAuthIsUsed spec = cas
Just auth ->
if not $ Auth.isUsernameAndPasswordAuthEnabled auth
then []
else
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
userEntityFields = Entity.getFields userEntity
in concatMap
(validateEntityHasField "app.auth.userEntity" userEntityFields)
[ ("username", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
("password", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
]
else validationErrors
where
validationErrors = concat [usernameValidationErrors, passwordValidationErrors]
usernameValidationErrors
| not $ null usernameTypeValidationErrors = usernameTypeValidationErrors
| otherwise = usernameAttributeValidationErrors
passwordValidationErrors =
validateEntityHasField
userEntityName
authUserEntityPath
userEntityFields
("password", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
usernameTypeValidationErrors =
validateEntityHasField
userEntityName
authUserEntityPath
userEntityFields
("username", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
usernameAttributeValidationErrors
| isFieldUnique "username" userEntity == Just True = []
| otherwise =
[ GenericValidationError $
"The field 'username' on entity '"
++ userEntityName
++ "' (referenced by "
++ authUserEntityPath
++ ") must be marked with the '@unique' attribute."
]
userEntityFields = Entity.getFields userEntity
authUserEntityPath = "app.auth.userEntity"
(userEntityName, userEntity) = AS.resolveRef spec (Auth.userEntity auth)
validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed :: AppSpec -> [ValidationError]
validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed spec = case App.auth (snd $ getApp spec) of
@ -165,10 +189,10 @@ validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed spec = case App.auth (sn
if not $ Auth.isEmailAuthEnabled auth
then []
else
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
let (userEntityName, userEntity) = AS.resolveRef spec (Auth.userEntity auth)
userEntityFields = Entity.getFields userEntity
in concatMap
(validateEntityHasField "app.auth.userEntity" userEntityFields)
(validateEntityHasField userEntityName "app.auth.userEntity" userEntityFields)
[ ("email", Entity.Field.FieldTypeComposite (Entity.Field.Optional Entity.Field.String), "String"),
("password", Entity.Field.FieldTypeComposite (Entity.Field.Optional Entity.Field.String), "String"),
("isEmailVerified", Entity.Field.FieldTypeScalar Entity.Field.Boolean, "Boolean"),
@ -203,7 +227,7 @@ validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec = case App.a
externalAuthEntityFields = Entity.getFields externalAuthEntity
externalAuthEntityValidationErrors =
concatMap
(validateEntityHasField "app.auth.externalAuthEntity" externalAuthEntityFields)
(validateEntityHasField externalAuthEntityName "app.auth.externalAuthEntity" externalAuthEntityFields)
[ ("provider", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
("providerId", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
("user", Entity.Field.FieldTypeScalar (Entity.Field.UserType userEntityName), userEntityName),
@ -211,20 +235,23 @@ validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec = case App.a
]
userEntityValidationErrors =
concatMap
(validateEntityHasField "app.auth.userEntity" userEntityFields)
[ ("externalAuthAssociations", Entity.Field.FieldTypeComposite $ Entity.Field.List $ Entity.Field.UserType externalAuthEntityName, externalAuthEntityName ++ "[]")
(validateEntityHasField userEntityName "app.auth.userEntity" userEntityFields)
[ ( "externalAuthAssociations",
Entity.Field.FieldTypeComposite $ Entity.Field.List $ Entity.Field.UserType externalAuthEntityName,
externalAuthEntityName ++ "[]"
)
]
in externalAuthEntityValidationErrors ++ userEntityValidationErrors
validateEntityHasField :: String -> [Entity.Field.Field] -> (String, Entity.Field.FieldType, String) -> [ValidationError]
validateEntityHasField entityName entityFields (fieldName, fieldType, fieldTypeName) =
let maybeField = find ((== fieldName) . Entity.Field.fieldName) entityFields
validateEntityHasField :: String -> String -> [Entity.Field.Field] -> (String, Entity.Field.FieldType, String) -> [ValidationError]
validateEntityHasField entityName authEntityPath entityFields (fieldName, fieldType, fieldTypeName) =
let maybeField = findFieldByName fieldName entityFields
in case maybeField of
Just providerField
| Entity.Field.fieldType providerField == fieldType -> []
_ ->
[ GenericValidationError $
"Expected an Entity referenced by " ++ entityName ++ " to have field '" ++ fieldName ++ "' of type '" ++ fieldTypeName ++ "'."
"Entity '" ++ entityName ++ "' (referenced by " ++ authEntityPath ++ ") must have field '" ++ fieldName ++ "' of type '" ++ fieldTypeName ++ "'."
]
validateApiRoutesAreUnique :: AppSpec -> [ValidationError]
@ -276,12 +303,26 @@ validateCrudOperations spec =
checkIfSimpleIdFieldIsDefinedForEntity :: (String, AS.Crud.Crud) -> [ValidationError]
checkIfSimpleIdFieldIsDefinedForEntity (crudName, crud) = case (maybeIdField, maybeIdBlockAttribute) of
(Just _, Nothing) -> []
(Nothing, Just _) -> [GenericValidationError $ "Entity referenced by \"" ++ crudName ++ "\" CRUD declaration must have an ID field (marked with @id attribute) and not a composite ID (defined with @@id attribute)."]
_missingIdFieldWithoutBlockIdAttributeDefined -> [GenericValidationError $ "Entity referenced by \"" ++ crudName ++ "\" CRUD declaration must have an ID field (marked with @id attribute)."]
(Nothing, Just _) ->
[ GenericValidationError $
"Entity '"
++ entityName
++ "' (referenced by CRUD declaration '"
++ crudName
++ "') must have an ID field (specified with the '@id' attribute) and not a composite ID (specified with the '@@id' attribute)."
]
_missingIdFieldWithoutBlockIdAttributeDefined ->
[ GenericValidationError $
"Entity '"
++ entityName
++ "' (referenced by CRUD declaration '"
++ crudName
++ "') must have an ID field (specified with the '@id' attribute)."
]
where
maybeIdField = Entity.getIdField entity
maybeIdBlockAttribute = Entity.getIdBlockAttribute entity
(_, entity) = AS.resolveRef spec (AS.Crud.entity crud)
(entityName, entity) = AS.resolveRef spec (AS.Crud.entity crud)
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
-- TODO: It would be great if we could ensure this at type level, but we decided that was too much work for now.
@ -313,7 +354,10 @@ doesUserEntityContainField spec fieldName = do
auth <- App.auth (snd $ getApp spec)
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
let userEntityFields = Entity.getFields userEntity
Just $ any (\field -> Entity.Field.fieldName field == fieldName) userEntityFields
Just $ isJust $ findFieldByName fieldName userEntityFields
findFieldByName :: String -> [Entity.Field.Field] -> Maybe Entity.Field.Field
findFieldByName name = find ((== name) . Entity.Field.fieldName)
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
-- We validated that entity field exists, so we can safely use fromJust here.

View File

@ -26,3 +26,6 @@ findIdBlockAttribute (PslModel.Body elements) = find isIdBlockAttribute attribut
-- We define the ID block attribute as an attribute with the name @@id.
idBlockAttributeName :: String
idBlockAttributeName = "id"
doesPslFieldHaveAttribute :: String -> PslModel.Field -> Bool
doesPslFieldHaveAttribute name PslModel.Field {_attrs = attrs} = any ((== name) . PslModel._attrName) attrs

View File

@ -8,10 +8,26 @@ import qualified Wasp.Psl.Ast.Model as PslModel
spec_AppSpecEntityTest :: Spec
spec_AppSpecEntityTest = do
describe "getIdField" $ do
it "gets primary field from entity when it exists" $ do
it "Gets primary field from entity when it exists" $ do
getIdField entityWithIdField `shouldBe` Just idField
it "returns Nothing if primary field doesn't exist" $ do
it "Returns Nothing if primary field doesn't exist" $ do
getIdField entityWithoutIdField `shouldBe` Nothing
describe "isFieldUnique" $ do
it "Returns Nothing if the field doesn't exist on the entity" $ do
Entity.isFieldUnique "nonExistingField" entityWithoutIdField `shouldBe` Nothing
it "Returns Just False if the field exists on the entity but isn't unique" $ do
Entity.isFieldUnique "description" entityWithIdField `shouldBe` Just False
it "Returns Just True if the field exists and is unique" $ do
Entity.isFieldUnique "id" entityWithIdField `shouldBe` Just True
describe "doesFieldHaveAttribute" $ do
it "Returns Nothing if the field doesn't exist on the entity" $ do
Entity.doesFieldHaveAttribute "nonExistingField" "unique" entityWithoutIdField `shouldBe` Nothing
it "Returns Just False if the field exists on the entity but doesn't have the required attribute" $ do
Entity.doesFieldHaveAttribute "description" "id" entityWithIdField `shouldBe` Just False
it "Returns Just True if the field exists on the entity and has the required attribute" $ do
Entity.doesFieldHaveAttribute "id" "id" entityWithIdField `shouldBe` Just True
where
entityWithIdField =
Entity.makeEntity $
@ -33,6 +49,10 @@ spec_AppSpecEntityTest = do
[ PslModel.Attribute
{ PslModel._attrName = "id",
PslModel._attrArgs = []
},
PslModel.Attribute
{ PslModel._attrName = "unique",
PslModel._attrArgs = []
}
],
PslModel._typeModifiers = []

View File

@ -21,6 +21,7 @@ import qualified Wasp.AppSpec.Page as AS.Page
import qualified Wasp.AppSpec.Route as AS.Route
import qualified Wasp.AppSpec.Valid as ASV
import qualified Wasp.Psl.Ast.Model as PslM
import qualified Wasp.Psl.Ast.Model as PslModel
import qualified Wasp.SemanticVersion as SV
import qualified Wasp.Version as WV
@ -87,10 +88,22 @@ spec_AppSpecValid = do
describe "auth-related validation" $ do
let userEntityName = "User"
let validUserField =
PslModel.Field
{ PslModel._name = "username",
PslModel._type = PslModel.String,
PslModel._attrs =
[ PslModel.Attribute
{ PslModel._attrName = "unique",
PslModel._attrArgs = []
}
],
PslModel._typeModifiers = []
}
let validUserEntity =
AS.Entity.makeEntity
( PslM.Body
[ PslM.ElementField $ makeBasicPslField "username" PslM.String,
[ PslM.ElementField validUserField,
PslM.ElementField $ makeBasicPslField "password" PslM.String
]
)
@ -224,7 +237,13 @@ spec_AppSpecValid = do
let invalidUserEntity2 =
AS.Entity.makeEntity
( PslM.Body
[ PslM.ElementField $ makeBasicPslField "username" PslM.String
[PslM.ElementField validUserField]
)
let invalidUserEntity3 =
AS.Entity.makeEntity
( PslM.Body
[ PslM.ElementField $ makeBasicPslField "username" PslM.String,
PslM.ElementField $ makeBasicPslField "password" PslM.String
]
)
@ -236,11 +255,15 @@ spec_AppSpecValid = do
it "returns an error if app.auth is set and user entity is of invalid shape" $ do
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity)
`shouldBe` [ ASV.GenericValidationError
"Expected an Entity referenced by app.auth.userEntity to have field 'username' of type 'String'."
"Entity 'User' (referenced by app.auth.userEntity) must have field 'username' of type 'String'."
]
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity2)
`shouldBe` [ ASV.GenericValidationError
"Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'String'."
"Entity 'User' (referenced by app.auth.userEntity) must have field 'password' of type 'String'."
]
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity3)
`shouldBe` [ ASV.GenericValidationError
"The field 'username' on entity 'User' (referenced by app.auth.userEntity) must be marked with the '@unique' attribute."
]
where
makeBasicPslField name typ = makePslField name typ False