From 13c49045d47ef685223941bb926a9d34bace14c8 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 27 Sep 2021 09:13:27 +0200 Subject: [PATCH] feat: Support acl authorization for IDP components Configuration has been updated so the IDP requests also pass through an Authorization component. A new config option was added to choose which authorization scheme to use for the IDP. --- config/default.json | 1 + config/dynamic.json | 1 + config/example-https-file.json | 1 + config/file-no-setup.json | 1 + config/file.json | 1 + config/http/handler/default.json | 1 + config/identity/README.md | 10 ++ config/identity/access/initializers/idp.json | 27 +++++ .../access/initializers/well-known.json | 27 +++++ config/identity/access/public.json | 13 ++ config/identity/access/restricted.json | 22 ++++ config/identity/handler/default.json | 35 ++++-- .../interaction/routes/forgot-password.json | 2 +- .../handler/interaction/routes/login.json | 2 +- .../interaction/routes/reset-password.json | 2 +- .../handler/interaction/routes/session.json | 2 +- .../registration/route/registration.json | 2 +- .../ldp/authentication/debug-auth-header.json | 2 +- .../ldp/authentication/debug-test-agent.json | 2 +- config/ldp/authentication/dpop-bearer.json | 2 +- config/ldp/authorization/allow-all.json | 5 + config/ldp/authorization/webacl.json | 10 ++ config/memory-subdomains.json | 1 + config/path-routing.json | 1 + config/restrict-idp.json | 42 +++++++ config/sparql-endpoint-no-setup.json | 1 + config/sparql-endpoint.json | 1 + .../EmptyCredentialsExtractor.ts | 21 ---- .../PublicCredentialsExtractor.ts | 12 ++ src/index.ts | 2 +- test/integration/DynamicPods.test.ts | 4 +- test/integration/Identity.test.ts | 12 +- test/integration/RestrictedIdentity.test.ts | 114 ++++++++++++++++++ test/integration/Setup.test.ts | 4 +- test/integration/Subdomains.test.ts | 4 +- test/integration/config/ldp-with-auth.json | 1 + test/integration/config/restricted-idp.json | 46 +++++++ .../config/server-dynamic-unsafe.json | 1 + test/integration/config/server-memory.json | 1 + .../config/server-subdomains-unsafe.json | 1 + test/integration/config/setup-memory.json | 1 + .../EmptyCredentialsExtractor.test.ts | 21 ---- .../PublicCredentialsExtractor.test.ts | 13 ++ test/util/Util.ts | 1 + 44 files changed, 401 insertions(+), 75 deletions(-) create mode 100644 config/identity/access/initializers/idp.json create mode 100644 config/identity/access/initializers/well-known.json create mode 100644 config/identity/access/public.json create mode 100644 config/identity/access/restricted.json create mode 100644 config/restrict-idp.json delete mode 100644 src/authentication/EmptyCredentialsExtractor.ts create mode 100644 src/authentication/PublicCredentialsExtractor.ts create mode 100644 test/integration/RestrictedIdentity.test.ts create mode 100644 test/integration/config/restricted-idp.json delete mode 100644 test/unit/authentication/EmptyCredentialsExtractor.test.ts create mode 100644 test/unit/authentication/PublicCredentialsExtractor.test.ts diff --git a/config/default.json b/config/default.json index 1d0d2456a..688c8ab27 100644 --- a/config/default.json +++ b/config/default.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/dynamic.json b/config/dynamic.json index 06f883598..af85dd9da 100644 --- a/config/dynamic.json +++ b/config/dynamic.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/example-https-file.json b/config/example-https-file.json index 05fc06a13..567255489 100644 --- a/config/example-https-file.json +++ b/config/example-https-file.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/file-no-setup.json b/config/file-no-setup.json index a151ed078..2ac7e9840 100644 --- a/config/file-no-setup.json +++ b/config/file-no-setup.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/file.json b/config/file.json index d92106691..457f3bcc0 100644 --- a/config/file.json +++ b/config/file.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/http/handler/default.json b/config/http/handler/default.json index 0de9adf89..3e2772f64 100644 --- a/config/http/handler/default.json +++ b/config/http/handler/default.json @@ -15,6 +15,7 @@ "handlers": [ { "@id": "urn:solid-server:default:StaticAssetHandler" }, { "@id": "urn:solid-server:default:SetupHandler" }, + { "@id": "urn:solid-server:default:AuthResourceHttpHandler" }, { "@id": "urn:solid-server:default:IdentityProviderHandler" }, { "@id": "urn:solid-server:default:LdpHandler" } ] diff --git a/config/identity/README.md b/config/identity/README.md index 80fd6d187..de1d88b7c 100644 --- a/config/identity/README.md +++ b/config/identity/README.md @@ -1,6 +1,16 @@ # Identity Options related to the Identity Provider. +## Access +Determines how publicly accessible some IDP features are. +* *public*: Everything is publicly accessible. +* *restricted*: The IDP components use the same authorization scheme as the main LDP component. + For example, if the server uses WebACL authorization and the registration endpoint is `/idp/register/`, + access to registration can be restricted by creating a valid `/idp/register/.acl` resource. + WARNING: This setting will write the necessary resources to the `.well-known` and IDP containers + to make this work. Again in the case of WebACL, this means ACL resources allowing full control access. + So make sure to update those two containers so only the correct credentials have the correct rights. + ## Email Necessary for sending e-mail when using IDP. * *default*: Disables e-mail functionality. diff --git a/config/identity/access/initializers/idp.json b/config/identity/access/initializers/idp.json new file mode 100644 index 000000000..e0047e2c8 --- /dev/null +++ b/config/identity/access/initializers/idp.json @@ -0,0 +1,27 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Makes sure the IDP container has the necessary root resources.", + "@id": "urn:solid-server:default:IdpContainerInitializer", + "@type": "ConditionalHandler", + "storageKey": "idpContainerInitialized", + "storageValue": true, + "storage": { "@id": "urn:solid-server:default:SetupStorage" }, + "source": { + "@type": "ContainerInitializer", + "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "args_path": "/idp/", + "args_store": { "@id": "urn:solid-server:default:ResourceStore" }, + "args_generator": { + "@type": "TemplatedResourcesGenerator", + "templateFolder": "@css:templates/root/empty", + "factory": { "@type": "ExtensionBasedMapperFactory" }, + "templateEngine": { "@type": "HandlebarsTemplateEngine" } + }, + "args_storageKey": "idpContainerInitialized", + "args_storage": { "@id": "urn:solid-server:default:SetupStorage" } + } + } + ] +} diff --git a/config/identity/access/initializers/well-known.json b/config/identity/access/initializers/well-known.json new file mode 100644 index 000000000..824d8a097 --- /dev/null +++ b/config/identity/access/initializers/well-known.json @@ -0,0 +1,27 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Makes sure the .well-known container has the necessary root resources. Some IDP resources are stored there due to OIDC requirements.", + "@id": "urn:solid-server:default:WellKnownContainerInitializer", + "@type": "ConditionalHandler", + "storageKey": "wellKnownContainerInitialized", + "storageValue": true, + "storage": { "@id": "urn:solid-server:default:SetupStorage" }, + "source": { + "@type": "ContainerInitializer", + "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "args_path": "/.well-known/", + "args_store": { "@id": "urn:solid-server:default:ResourceStore" }, + "args_generator": { + "@type": "TemplatedResourcesGenerator", + "templateFolder": "@css:templates/root/empty", + "factory": { "@type": "ExtensionBasedMapperFactory" }, + "templateEngine": { "@type": "HandlebarsTemplateEngine" } + }, + "args_storageKey": "wellKnownContainerInitialized", + "args_storage": { "@id": "urn:solid-server:default:SetupStorage" } + } + } + ] +} diff --git a/config/identity/access/public.json b/config/identity/access/public.json new file mode 100644 index 000000000..d857e0602 --- /dev/null +++ b/config/identity/access/public.json @@ -0,0 +1,13 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Allow everyone to register new pods.", + "@id": "urn:solid-server:default:IdentityProviderAuthorizingHandler", + "AuthorizingHttpHandler:_args_permissionReader": { + "@type": "AllStaticReader", + "allow": true + } + } + ] +} diff --git a/config/identity/access/restricted.json b/config/identity/access/restricted.json new file mode 100644 index 000000000..6c0a504c1 --- /dev/null +++ b/config/identity/access/restricted.json @@ -0,0 +1,22 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "import": [ + "files-scs:config/identity/access/initializers/idp.json", + "files-scs:config/identity/access/initializers/well-known.json" + ], + "@graph": [ + { + "comment": "Use the same authorization for IDP components as is used for LDP, such that for instance registration can be restricted to certain agents.", + "@id": "urn:solid-server:default:IdentityProviderAuthorizingHandler", + "AuthorizingHttpHandler:_args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" } + }, + { + "comment": "IDP-related containers require initialized resources to support authorization.", + "@id": "urn:solid-server:default:ParallelInitializer", + "ParallelHandler:_handlers": [ + { "@id": "urn:solid-server:default:IdpContainerInitializer" }, + { "@id": "urn:solid-server:default:WellKnownContainerInitializer" } + ] + } + ] +} diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json index 39bab35c0..b9851ec00 100644 --- a/config/identity/handler/default.json +++ b/config/identity/handler/default.json @@ -18,6 +18,7 @@ "args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" } }, { + "comment": "Handles IDP input parsing.", "@id": "urn:solid-server:default:IdentityProviderParsingHandler", "@type": "ParsingHttpHandler", "args_requestParser": { "@id": "urn:solid-server:default:RequestParser" }, @@ -25,19 +26,29 @@ "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "args_operationHandler": { - "@id": "urn:solid-server:default:IdentityProviderHttpHandler", - "@type": "IdentityProviderHttpHandler", - "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, - "args_idpPath": "/idp", - "args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, - "args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, - "args_interactionCompleter": { - "comment": "Responsible for finishing OIDC interactions.", - "@type": "InteractionCompleter", - "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" } - }, - "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" } + "comment": "Handles IDP input authorization. Permission reader should be set to allow all if no authorization is needed.", + "@type": "AuthorizingHttpHandler", + "@id": "urn:solid-server:default:IdentityProviderAuthorizingHandler", + "args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" }, + "args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" }, + "args_authorizer": { "@id": "urn:solid-server:default:Authorizer" }, + "args_operationHandler": { "@id": "urn:solid-server:default:IdentityProviderHttpHandler" } } + }, + { + "comment": "Handles IDP handler behaviour.", + "@id": "urn:solid-server:default:IdentityProviderHttpHandler", + "@type": "IdentityProviderHttpHandler", + "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "args_idpPath": "/idp", + "args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, + "args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, + "args_interactionCompleter": { + "comment": "Responsible for finishing OIDC interactions.", + "@type": "InteractionCompleter", + "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" } + }, + "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" } } ] } diff --git a/config/identity/handler/interaction/routes/forgot-password.json b/config/identity/handler/interaction/routes/forgot-password.json index 021dc2b61..351288ac9 100644 --- a/config/identity/handler/interaction/routes/forgot-password.json +++ b/config/identity/handler/interaction/routes/forgot-password.json @@ -5,7 +5,7 @@ "comment": "Handles all functionality on the forgot password page", "@id": "urn:solid-server:auth:password:ForgotPasswordRoute", "@type": "BasicInteractionRoute", - "route": "^/forgotpassword/?$", + "route": "^/forgotpassword/$", "viewTemplates": { "BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/forgot-password.html.ejs" diff --git a/config/identity/handler/interaction/routes/login.json b/config/identity/handler/interaction/routes/login.json index 994e0c6cd..007709841 100644 --- a/config/identity/handler/interaction/routes/login.json +++ b/config/identity/handler/interaction/routes/login.json @@ -5,7 +5,7 @@ "comment": "Handles all functionality on the Login Page", "@id": "urn:solid-server:auth:password:LoginRoute", "@type": "BasicInteractionRoute", - "route": "^/login/?$", + "route": "^/login/$", "prompt": "login", "viewTemplates": { "BasicInteractionRoute:_viewTemplates_key": "text/html", diff --git a/config/identity/handler/interaction/routes/reset-password.json b/config/identity/handler/interaction/routes/reset-password.json index b040e5cb7..40da5a364 100644 --- a/config/identity/handler/interaction/routes/reset-password.json +++ b/config/identity/handler/interaction/routes/reset-password.json @@ -6,7 +6,7 @@ "comment": "Handles the reset password page submission", "@id": "urn:solid-server:auth:password:ResetPasswordRoute", "@type": "BasicInteractionRoute", - "route": "^/resetpassword(/[^/]*)?$", + "route": "^/resetpassword/[^/]*$", "viewTemplates": { "BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/reset-password.html.ejs" diff --git a/config/identity/handler/interaction/routes/session.json b/config/identity/handler/interaction/routes/session.json index a9c238578..8a292288a 100644 --- a/config/identity/handler/interaction/routes/session.json +++ b/config/identity/handler/interaction/routes/session.json @@ -5,7 +5,7 @@ "comment": "Handles confirm requests", "@id": "urn:solid-server:auth:password:SessionRoute", "@type": "BasicInteractionRoute", - "route": "^/confirm/?$", + "route": "^/confirm/$", "prompt": "consent", "viewTemplates": { "BasicInteractionRoute:_viewTemplates_key": "text/html", diff --git a/config/identity/registration/route/registration.json b/config/identity/registration/route/registration.json index 7623ae7a6..7df8f15f4 100644 --- a/config/identity/registration/route/registration.json +++ b/config/identity/registration/route/registration.json @@ -5,7 +5,7 @@ "comment": "Handles all functionality on the register page", "@id": "urn:solid-server:auth:password:RegistrationRoute", "@type": "BasicInteractionRoute", - "route": "^/register/?$", + "route": "^/register/$", "viewTemplates": { "BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/register.html.ejs" diff --git a/config/ldp/authentication/debug-auth-header.json b/config/ldp/authentication/debug-auth-header.json index 553143c4a..a85ece254 100644 --- a/config/ldp/authentication/debug-auth-header.json +++ b/config/ldp/authentication/debug-auth-header.json @@ -10,7 +10,7 @@ "@type": "UnionCredentialsExtractor", "extractors": [ { "@type": "UnsecureWebIdExtractor" }, - { "@type": "EmptyCredentialsExtractor" } + { "@type": "PublicCredentialsExtractor" } ] } ] diff --git a/config/ldp/authentication/debug-test-agent.json b/config/ldp/authentication/debug-test-agent.json index 798e78b52..30659f4c3 100644 --- a/config/ldp/authentication/debug-test-agent.json +++ b/config/ldp/authentication/debug-test-agent.json @@ -13,7 +13,7 @@ "@type": "UnsecureConstantCredentialsExtractor", "agent": "http://test.com/card#me" }, - { "@type": "EmptyCredentialsExtractor" } + { "@type": "PublicCredentialsExtractor" } ] } ] diff --git a/config/ldp/authentication/dpop-bearer.json b/config/ldp/authentication/dpop-bearer.json index c7df44cdc..7aa4988d7 100644 --- a/config/ldp/authentication/dpop-bearer.json +++ b/config/ldp/authentication/dpop-bearer.json @@ -18,7 +18,7 @@ { "@type": "BearerWebIdExtractor" } ] }, - { "@type": "EmptyCredentialsExtractor" } + { "@type": "PublicCredentialsExtractor" } ] } ] diff --git a/config/ldp/authorization/allow-all.json b/config/ldp/authorization/allow-all.json index 4002792a3..1118a0cef 100644 --- a/config/ldp/authorization/allow-all.json +++ b/config/ldp/authorization/allow-all.json @@ -9,6 +9,11 @@ "@id": "urn:solid-server:default:PermissionReader", "@type": "AllStaticReader", "allow": true + }, + { + "comment": "Everything is allowed, so there are no auth-specific resources.", + "@id": "urn:solid-server:default:AuthResourceHttpHandler", + "@type": "UnsupportedAsyncHandler" } ] } diff --git a/config/ldp/authorization/webacl.json b/config/ldp/authorization/webacl.json index 286d7db7b..4e23d9e4f 100644 --- a/config/ldp/authorization/webacl.json +++ b/config/ldp/authorization/webacl.json @@ -25,6 +25,16 @@ }, { "@id": "urn:solid-server:default:WebAclReader" } ] + }, + { + "comment": "In case of WebACL authorization the ACL resources determine authorization.", + "@id": "urn:solid-server:default:AuthResourceHttpHandler", + "@type": "RouterHandler", + "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" }, + "args_allowedMethods": [ "*" ], + "args_allowedPathNames": [ "^/.*\\.acl$" ], + "args_handler": { "@id": "urn:solid-server:default:LdpHandler" } } ] } diff --git a/config/memory-subdomains.json b/config/memory-subdomains.json index e1f057c3f..35e3d0d71 100644 --- a/config/memory-subdomains.json +++ b/config/memory-subdomains.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/path-routing.json b/config/path-routing.json index 16ff10a92..12d8474a9 100644 --- a/config/path-routing.json +++ b/config/path-routing.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/restrict-idp.json b/config/restrict-idp.json new file mode 100644 index 000000000..eab1ea613 --- /dev/null +++ b/config/restrict-idp.json @@ -0,0 +1,42 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "import": [ + "files-scs:config/app/main/default.json", + "files-scs:config/app/init/default.json", + "files-scs:config/app/setup/disabled.json", + "files-scs:config/http/handler/default.json", + "files-scs:config/http/middleware/websockets.json", + "files-scs:config/http/server-factory/websockets.json", + "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/restricted.json", + "files-scs:config/identity/email/default.json", + "files-scs:config/identity/handler/default.json", + "files-scs:config/identity/ownership/token.json", + "files-scs:config/identity/pod/static.json", + "files-scs:config/identity/registration/enabled.json", + "files-scs:config/ldp/authentication/dpop-bearer.json", + "files-scs:config/ldp/authorization/webacl.json", + "files-scs:config/ldp/handler/default.json", + "files-scs:config/ldp/metadata-parser/default.json", + "files-scs:config/ldp/metadata-writer/default.json", + "files-scs:config/ldp/modes/default.json", + "files-scs:config/storage/backend/file.json", + "files-scs:config/storage/key-value/resource-store.json", + "files-scs:config/storage/middleware/default.json", + "files-scs:config/util/auxiliary/acl.json", + "files-scs:config/util/identifiers/suffix.json", + "files-scs:config/util/index/default.json", + "files-scs:config/util/logging/winston.json", + "files-scs:config/util/representation-conversion/default.json", + "files-scs:config/util/resource-locker/memory.json", + "files-scs:config/util/variables/default.json" + ], + "@graph": [ + { + "comment": [ + "This server uses a file backend and allows restricting the access to IDP components using WebACL.", + "Make sure to read the documentation about the config/identity/access configuration." + ] + } + ] +} diff --git a/config/sparql-endpoint-no-setup.json b/config/sparql-endpoint-no-setup.json index 6c9b94a98..caa432a46 100644 --- a/config/sparql-endpoint-no-setup.json +++ b/config/sparql-endpoint-no-setup.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json index d6af94502..9e22b67fb 100644 --- a/config/sparql-endpoint.json +++ b/config/sparql-endpoint.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/src/authentication/EmptyCredentialsExtractor.ts b/src/authentication/EmptyCredentialsExtractor.ts deleted file mode 100644 index 847aeabbc..000000000 --- a/src/authentication/EmptyCredentialsExtractor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { HttpRequest } from '../server/HttpRequest'; -import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; -import { CredentialGroup } from './Credentials'; -import type { CredentialSet } from './Credentials'; -import { CredentialsExtractor } from './CredentialsExtractor'; - -/** - * Extracts the empty credentials, indicating an unauthenticated agent. - */ -export class EmptyCredentialsExtractor extends CredentialsExtractor { - public async canHandle({ headers }: HttpRequest): Promise { - const { authorization } = headers; - if (authorization) { - throw new NotImplementedHttpError('Unexpected Authorization scheme.'); - } - } - - public async handle(): Promise { - return { [CredentialGroup.public]: {}}; - } -} diff --git a/src/authentication/PublicCredentialsExtractor.ts b/src/authentication/PublicCredentialsExtractor.ts new file mode 100644 index 000000000..f8df265b9 --- /dev/null +++ b/src/authentication/PublicCredentialsExtractor.ts @@ -0,0 +1,12 @@ +import { CredentialGroup } from './Credentials'; +import type { CredentialSet } from './Credentials'; +import { CredentialsExtractor } from './CredentialsExtractor'; + +/** + * Extracts the public credentials, to be used for data everyone has access to. + */ +export class PublicCredentialsExtractor extends CredentialsExtractor { + public async handle(): Promise { + return { [CredentialGroup.public]: {}}; + } +} diff --git a/src/index.ts b/src/index.ts index 217dd2579..2bf20b9bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export * from './authentication/BearerWebIdExtractor'; export * from './authentication/Credentials'; export * from './authentication/CredentialsExtractor'; export * from './authentication/DPoPWebIdExtractor'; -export * from './authentication/EmptyCredentialsExtractor'; +export * from './authentication/PublicCredentialsExtractor'; export * from './authentication/UnionCredentialsExtractor'; export * from './authentication/UnsecureConstantCredentialsExtractor'; export * from './authentication/UnsecureWebIdExtractor'; diff --git a/test/integration/DynamicPods.test.ts b/test/integration/DynamicPods.test.ts index a8e22de90..7aa22ec26 100644 --- a/test/integration/DynamicPods.test.ts +++ b/test/integration/DynamicPods.test.ts @@ -61,7 +61,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template }); it('creates a pod with the given config.', async(): Promise => { - const res = await fetch(`${baseUrl}idp/register`, { + const res = await fetch(`${baseUrl}idp/register/`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(settings), @@ -118,7 +118,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template it('should not be able to create a pod with the same name.', async(): Promise => { const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' }; - const res = await fetch(`${baseUrl}idp/register`, { + const res = await fetch(`${baseUrl}idp/register/`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(newSettings), diff --git a/test/integration/Identity.test.ts b/test/integration/Identity.test.ts index 04d8dbc84..41a9f868a 100644 --- a/test/integration/Identity.test.ts +++ b/test/integration/Identity.test.ts @@ -96,7 +96,7 @@ describe('A Solid server with IDP', (): void => { }); it('sends the form once to receive the registration triple.', async(): Promise => { - const res = await postForm(`${baseUrl}idp/register`, formBody); + const res = await postForm(`${baseUrl}idp/register/`, formBody); expect(res.status).toBe(400); registrationTriple = extractRegistrationTriple(await res.text(), webId); }); @@ -112,7 +112,7 @@ describe('A Solid server with IDP', (): void => { }); it('sends the form again to successfully register.', async(): Promise => { - const res = await postForm(`${baseUrl}idp/register`, formBody); + const res = await postForm(`${baseUrl}idp/register/`, formBody); expect(res.status).toBe(200); const text = await res.text(); expect(text).toMatch(new RegExp(`your.WebID.*${webId}`, 'u')); @@ -184,7 +184,7 @@ describe('A Solid server with IDP', (): void => { let nextUrl: string; it('sends the corresponding email address through the form to get a mail.', async(): Promise => { - const res = await postForm(`${baseUrl}idp/forgotpassword`, stringify({ email })); + const res = await postForm(`${baseUrl}idp/forgotpassword/`, stringify({ email })); expect(res.status).toBe(200); expect(load(await res.text())('form p').first().text().trim()) .toBe('If your account exists, an email has been sent with a link to reset your password.'); @@ -260,7 +260,7 @@ describe('A Solid server with IDP', (): void => { }); it('sends the form once to receive the registration triple.', async(): Promise => { - const res = await postForm(`${baseUrl}idp/register`, formBody); + const res = await postForm(`${baseUrl}idp/register/`, formBody); expect(res.status).toBe(400); registrationTriple = extractRegistrationTriple(await res.text(), webId2); }); @@ -276,7 +276,7 @@ describe('A Solid server with IDP', (): void => { }); it('sends the form again to successfully register.', async(): Promise => { - const res = await postForm(`${baseUrl}idp/register`, formBody); + const res = await postForm(`${baseUrl}idp/register/`, formBody); expect(res.status).toBe(200); const text = await res.text(); expect(text).toMatch(new RegExp(`Your new Pod.*${baseUrl}${podName}/`, 'u')); @@ -294,7 +294,7 @@ describe('A Solid server with IDP', (): void => { }); it('sends the form to create the WebID and register.', async(): Promise => { - const res = await postForm(`${baseUrl}idp/register`, formBody); + const res = await postForm(`${baseUrl}idp/register/`, formBody); expect(res.status).toBe(200); const text = await res.text(); diff --git a/test/integration/RestrictedIdentity.test.ts b/test/integration/RestrictedIdentity.test.ts new file mode 100644 index 000000000..3d726a1aa --- /dev/null +++ b/test/integration/RestrictedIdentity.test.ts @@ -0,0 +1,114 @@ +import { fetch } from 'cross-fetch'; +import type { App } from '../../src/init/App'; +import { joinUrl } from '../../src/util/PathUtil'; +import { getPort } from '../util/Util'; +import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config'; +import { IdentityTestState } from './IdentityTestState'; + +const port = getPort('RestrictedIdentity'); +const baseUrl = `http://localhost:${port}/`; + +// Undo the global access token verifier mock +jest.unmock('@solid/access-token-verifier'); + +// Prevent panva/node-openid-client from emitting DraftWarning +jest.spyOn(process, 'emitWarning').mockImplementation(); + +describe('A server with restricted IDP access', (): void => { + let app: App; + const settings = { + podName: 'alice', + email: 'alice@test.email', + password: 'password', + confirmPassword: 'password', + createWebId: true, + register: true, + createPod: true, + }; + const webId = joinUrl(baseUrl, 'alice/profile/card#me'); + + beforeAll(async(): Promise => { + const instances = await instantiateFromConfig( + 'urn:solid-server:test:Instances', + getTestConfigPath('restricted-idp.json'), + getDefaultVariables(port, baseUrl), + ) as Record; + ({ app } = instances); + await app.start(); + }); + + afterAll(async(): Promise => { + await app.stop(); + }); + + it('has ACL resources in the relevant containers.', async(): Promise => { + let res = await fetch(joinUrl(baseUrl, '.well-known/.acl')); + expect(res.status).toBe(200); + + res = await fetch(joinUrl(baseUrl, 'idp/.acl')); + expect(res.status).toBe(200); + }); + + it('can create a pod.', async(): Promise => { + const res = await fetch(`${baseUrl}idp/register/`, { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify(settings), + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.webId).toBe(webId); + }); + + it('can restrict registration access.', async(): Promise => { + // Only allow new WebID to register + const restrictedAcl = `@prefix acl: . +@prefix foaf: . + +<#authorization> + a acl:Authorization; + acl:agent <${webId}>; + acl:mode acl:Read, acl:Write, acl:Control; + acl:accessTo <./>.`; + + let res = await fetch(`${baseUrl}idp/register/.acl`, { + method: 'PUT', + headers: { 'content-type': 'text/turtle' }, + body: restrictedAcl, + }); + expect(res.status).toBe(205); + + // Registration is now disabled + res = await fetch(`${baseUrl}idp/register/`); + expect(res.status).toBe(401); + + res = await fetch(`${baseUrl}idp/register/`, { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify({ ...settings, email: 'bob@test.email', podName: 'bob' }), + }); + expect(res.status).toBe(401); + }); + + it('can still access registration with the correct credentials.', async(): Promise => { + // Logging into session + const state = new IdentityTestState(baseUrl, 'http://mockedredirect/', baseUrl); + const url = await state.startSession(); + await state.parseLoginPage(url); + await state.login(url, settings.email, settings.password); + expect(state.session.info?.webId).toBe(webId); + + // Registration still works for this WebID + let res = await state.session.fetch(`${baseUrl}idp/register/`); + expect(res.status).toBe(200); + + res = await state.session.fetch(`${baseUrl}idp/register/`, { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify({ ...settings, email: 'bob@test.email', podName: 'bob' }), + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.webId).toBe(joinUrl(baseUrl, 'bob/profile/card#me')); + }); +}); diff --git a/test/integration/Setup.test.ts b/test/integration/Setup.test.ts index 8f943fbb4..8cec739c4 100644 --- a/test/integration/Setup.test.ts +++ b/test/integration/Setup.test.ts @@ -55,7 +55,7 @@ describe('A Solid server with setup', (): void => { // Registration still possible const registerParams = { email, podName, password, confirmPassword: password, createWebId: true }; - res = await fetch(joinUrl(baseUrl, 'idp/register'), { + res = await fetch(joinUrl(baseUrl, 'idp/register/'), { method: 'POST', headers: { accept: 'text/html', 'content-type': 'application/json' }, body: JSON.stringify(registerParams), @@ -83,7 +83,7 @@ describe('A Solid server with setup', (): void => { // Root pod registration is never allowed const registerParams = { email, podName, password, confirmPassword: password, createWebId: true, rootPod: true }; - res = await fetch(joinUrl(baseUrl, 'idp/register'), { + res = await fetch(joinUrl(baseUrl, 'idp/register/'), { method: 'POST', headers: { accept: 'text/html', 'content-type': 'application/json' }, body: JSON.stringify(registerParams), diff --git a/test/integration/Subdomains.test.ts b/test/integration/Subdomains.test.ts index f96e624b7..f50e7951b 100644 --- a/test/integration/Subdomains.test.ts +++ b/test/integration/Subdomains.test.ts @@ -89,7 +89,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo describe('handling pods', (): void => { it('creates pods in a subdomain.', async(): Promise => { - const res = await fetch(`${baseUrl}idp/register`, { + const res = await fetch(`${baseUrl}idp/register/`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(settings), @@ -150,7 +150,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo it('should not be able to create a pod with the same name.', async(): Promise => { const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' }; - const res = await fetch(`${baseUrl}idp/register`, { + const res = await fetch(`${baseUrl}idp/register/`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(newSettings), diff --git a/test/integration/config/ldp-with-auth.json b/test/integration/config/ldp-with-auth.json index 7f2266b07..c7cab9d55 100644 --- a/test/integration/config/ldp-with-auth.json +++ b/test/integration/config/ldp-with-auth.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/handler/default.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/webacl.json", diff --git a/test/integration/config/restricted-idp.json b/test/integration/config/restricted-idp.json new file mode 100644 index 000000000..9537dcf67 --- /dev/null +++ b/test/integration/config/restricted-idp.json @@ -0,0 +1,46 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "import": [ + "files-scs:config/app/main/default.json", + "files-scs:config/app/init/default.json", + "files-scs:config/app/setup/disabled.json", + "files-scs:config/http/handler/default.json", + "files-scs:config/http/middleware/websockets.json", + "files-scs:config/http/server-factory/websockets.json", + "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/restricted.json", + "files-scs:config/identity/email/default.json", + "files-scs:config/identity/handler/default.json", + "files-scs:config/identity/ownership/token.json", + "files-scs:config/identity/pod/static.json", + "files-scs:config/identity/registration/enabled.json", + "files-scs:config/ldp/authentication/dpop-bearer.json", + "files-scs:config/ldp/authorization/webacl.json", + "files-scs:config/ldp/handler/default.json", + "files-scs:config/ldp/metadata-parser/default.json", + "files-scs:config/ldp/metadata-writer/default.json", + "files-scs:config/ldp/modes/default.json", + "files-scs:config/storage/backend/memory.json", + "files-scs:config/storage/key-value/resource-store.json", + "files-scs:config/storage/middleware/default.json", + "files-scs:config/util/auxiliary/acl.json", + "files-scs:config/util/identifiers/suffix.json", + "files-scs:config/util/index/default.json", + "files-scs:config/util/logging/winston.json", + "files-scs:config/util/representation-conversion/default.json", + "files-scs:config/util/resource-locker/memory.json", + "files-scs:config/util/variables/default.json" + ], + "@graph": [ + { + "@id": "urn:solid-server:test:Instances", + "@type": "RecordObject", + "RecordObject:_record": [ + { + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } + } + ] + } + ] +} diff --git a/test/integration/config/server-dynamic-unsafe.json b/test/integration/config/server-dynamic-unsafe.json index d95ff54f6..643805b23 100644 --- a/test/integration/config/server-dynamic-unsafe.json +++ b/test/integration/config/server-dynamic-unsafe.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/unsafe-no-check.json", diff --git a/test/integration/config/server-memory.json b/test/integration/config/server-memory.json index 9953f3e91..7edca1d46 100644 --- a/test/integration/config/server-memory.json +++ b/test/integration/config/server-memory.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", diff --git a/test/integration/config/server-subdomains-unsafe.json b/test/integration/config/server-subdomains-unsafe.json index 096aa615b..28c52299b 100644 --- a/test/integration/config/server-subdomains-unsafe.json +++ b/test/integration/config/server-subdomains-unsafe.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/unsafe-no-check.json", diff --git a/test/integration/config/setup-memory.json b/test/integration/config/setup-memory.json index 72e282f1f..8daca4d59 100644 --- a/test/integration/config/setup-memory.json +++ b/test/integration/config/setup-memory.json @@ -8,6 +8,7 @@ "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", + "files-scs:config/identity/access/public.json", "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", diff --git a/test/unit/authentication/EmptyCredentialsExtractor.test.ts b/test/unit/authentication/EmptyCredentialsExtractor.test.ts deleted file mode 100644 index 61f1bc731..000000000 --- a/test/unit/authentication/EmptyCredentialsExtractor.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CredentialGroup } from '../../../src/authentication/Credentials'; -import { EmptyCredentialsExtractor } from '../../../src/authentication/EmptyCredentialsExtractor'; -import type { HttpRequest } from '../../../src/server/HttpRequest'; -import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError'; - -describe('An EmptyCredentialsExtractor', (): void => { - const extractor = new EmptyCredentialsExtractor(); - - it('throws an error if an Authorization header is specified.', async(): Promise => { - const headers = { authorization: 'Other http://alice.example/card#me' }; - const result = extractor.handleSafe({ headers } as HttpRequest); - await expect(result).rejects.toThrow(NotImplementedHttpError); - await expect(result).rejects.toThrow('Unexpected Authorization scheme.'); - }); - - it('returns the empty credentials.', async(): Promise => { - const headers = {}; - const result = extractor.handleSafe({ headers } as HttpRequest); - await expect(result).resolves.toEqual({ [CredentialGroup.public]: {}}); - }); -}); diff --git a/test/unit/authentication/PublicCredentialsExtractor.test.ts b/test/unit/authentication/PublicCredentialsExtractor.test.ts new file mode 100644 index 000000000..d8e9f2958 --- /dev/null +++ b/test/unit/authentication/PublicCredentialsExtractor.test.ts @@ -0,0 +1,13 @@ +import { CredentialGroup } from '../../../src/authentication/Credentials'; +import { PublicCredentialsExtractor } from '../../../src/authentication/PublicCredentialsExtractor'; +import type { HttpRequest } from '../../../src/server/HttpRequest'; + +describe('A PublicCredentialsExtractor', (): void => { + const extractor = new PublicCredentialsExtractor(); + + it('returns the empty credentials.', async(): Promise => { + const headers = {}; + const result = extractor.handleSafe({ headers } as HttpRequest); + await expect(result).resolves.toEqual({ [CredentialGroup.public]: {}}); + }); +}); diff --git a/test/util/Util.ts b/test/util/Util.ts index 5bf7b856a..a5481a5c9 100644 --- a/test/util/Util.ts +++ b/test/util/Util.ts @@ -12,6 +12,7 @@ const portNames = [ 'Middleware', 'PodCreation', 'RedisResourceLocker', + 'RestrictedIdentity', 'ServerFetch', 'SetupMemory', 'SparqlStorage',