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.
This commit is contained in:
Joachim Van Herwegen 2021-09-27 09:13:27 +02:00
parent 9968f2ae5b
commit 13c49045d4
44 changed files with 401 additions and 75 deletions

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -15,6 +15,7 @@
"handlers": [ "handlers": [
{ "@id": "urn:solid-server:default:StaticAssetHandler" }, { "@id": "urn:solid-server:default:StaticAssetHandler" },
{ "@id": "urn:solid-server:default:SetupHandler" }, { "@id": "urn:solid-server:default:SetupHandler" },
{ "@id": "urn:solid-server:default:AuthResourceHttpHandler" },
{ "@id": "urn:solid-server:default:IdentityProviderHandler" }, { "@id": "urn:solid-server:default:IdentityProviderHandler" },
{ "@id": "urn:solid-server:default:LdpHandler" } { "@id": "urn:solid-server:default:LdpHandler" }
] ]

View File

@ -1,6 +1,16 @@
# Identity # Identity
Options related to the Identity Provider. 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 ## Email
Necessary for sending e-mail when using IDP. Necessary for sending e-mail when using IDP.
* *default*: Disables e-mail functionality. * *default*: Disables e-mail functionality.

View File

@ -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" }
}
}
]
}

View File

@ -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" }
}
}
]
}

View File

@ -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
}
}
]
}

View File

@ -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" }
]
}
]
}

View File

@ -18,6 +18,7 @@
"args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" } "args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
}, },
{ {
"comment": "Handles IDP input parsing.",
"@id": "urn:solid-server:default:IdentityProviderParsingHandler", "@id": "urn:solid-server:default:IdentityProviderParsingHandler",
"@type": "ParsingHttpHandler", "@type": "ParsingHttpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" }, "args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
@ -25,19 +26,29 @@
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationHandler": { "args_operationHandler": {
"@id": "urn:solid-server:default:IdentityProviderHttpHandler", "comment": "Handles IDP input authorization. Permission reader should be set to allow all if no authorization is needed.",
"@type": "IdentityProviderHttpHandler", "@type": "AuthorizingHttpHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "@id": "urn:solid-server:default:IdentityProviderAuthorizingHandler",
"args_idpPath": "/idp", "args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
"args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, "args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, "args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
"args_interactionCompleter": { "args_operationHandler": { "@id": "urn:solid-server:default:IdentityProviderHttpHandler" }
"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 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" }
} }
] ]
} }

View File

@ -5,7 +5,7 @@
"comment": "Handles all functionality on the forgot password page", "comment": "Handles all functionality on the forgot password page",
"@id": "urn:solid-server:auth:password:ForgotPasswordRoute", "@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
"@type": "BasicInteractionRoute", "@type": "BasicInteractionRoute",
"route": "^/forgotpassword/?$", "route": "^/forgotpassword/$",
"viewTemplates": { "viewTemplates": {
"BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_key": "text/html",
"BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/forgot-password.html.ejs" "BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/forgot-password.html.ejs"

View File

@ -5,7 +5,7 @@
"comment": "Handles all functionality on the Login Page", "comment": "Handles all functionality on the Login Page",
"@id": "urn:solid-server:auth:password:LoginRoute", "@id": "urn:solid-server:auth:password:LoginRoute",
"@type": "BasicInteractionRoute", "@type": "BasicInteractionRoute",
"route": "^/login/?$", "route": "^/login/$",
"prompt": "login", "prompt": "login",
"viewTemplates": { "viewTemplates": {
"BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_key": "text/html",

View File

@ -6,7 +6,7 @@
"comment": "Handles the reset password page submission", "comment": "Handles the reset password page submission",
"@id": "urn:solid-server:auth:password:ResetPasswordRoute", "@id": "urn:solid-server:auth:password:ResetPasswordRoute",
"@type": "BasicInteractionRoute", "@type": "BasicInteractionRoute",
"route": "^/resetpassword(/[^/]*)?$", "route": "^/resetpassword/[^/]*$",
"viewTemplates": { "viewTemplates": {
"BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_key": "text/html",
"BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/reset-password.html.ejs" "BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/reset-password.html.ejs"

View File

@ -5,7 +5,7 @@
"comment": "Handles confirm requests", "comment": "Handles confirm requests",
"@id": "urn:solid-server:auth:password:SessionRoute", "@id": "urn:solid-server:auth:password:SessionRoute",
"@type": "BasicInteractionRoute", "@type": "BasicInteractionRoute",
"route": "^/confirm/?$", "route": "^/confirm/$",
"prompt": "consent", "prompt": "consent",
"viewTemplates": { "viewTemplates": {
"BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_key": "text/html",

View File

@ -5,7 +5,7 @@
"comment": "Handles all functionality on the register page", "comment": "Handles all functionality on the register page",
"@id": "urn:solid-server:auth:password:RegistrationRoute", "@id": "urn:solid-server:auth:password:RegistrationRoute",
"@type": "BasicInteractionRoute", "@type": "BasicInteractionRoute",
"route": "^/register/?$", "route": "^/register/$",
"viewTemplates": { "viewTemplates": {
"BasicInteractionRoute:_viewTemplates_key": "text/html", "BasicInteractionRoute:_viewTemplates_key": "text/html",
"BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/register.html.ejs" "BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/register.html.ejs"

View File

@ -10,7 +10,7 @@
"@type": "UnionCredentialsExtractor", "@type": "UnionCredentialsExtractor",
"extractors": [ "extractors": [
{ "@type": "UnsecureWebIdExtractor" }, { "@type": "UnsecureWebIdExtractor" },
{ "@type": "EmptyCredentialsExtractor" } { "@type": "PublicCredentialsExtractor" }
] ]
} }
] ]

View File

@ -13,7 +13,7 @@
"@type": "UnsecureConstantCredentialsExtractor", "@type": "UnsecureConstantCredentialsExtractor",
"agent": "http://test.com/card#me" "agent": "http://test.com/card#me"
}, },
{ "@type": "EmptyCredentialsExtractor" } { "@type": "PublicCredentialsExtractor" }
] ]
} }
] ]

View File

@ -18,7 +18,7 @@
{ "@type": "BearerWebIdExtractor" } { "@type": "BearerWebIdExtractor" }
] ]
}, },
{ "@type": "EmptyCredentialsExtractor" } { "@type": "PublicCredentialsExtractor" }
] ]
} }
] ]

View File

@ -9,6 +9,11 @@
"@id": "urn:solid-server:default:PermissionReader", "@id": "urn:solid-server:default:PermissionReader",
"@type": "AllStaticReader", "@type": "AllStaticReader",
"allow": true "allow": true
},
{
"comment": "Everything is allowed, so there are no auth-specific resources.",
"@id": "urn:solid-server:default:AuthResourceHttpHandler",
"@type": "UnsupportedAsyncHandler"
} }
] ]
} }

View File

@ -25,6 +25,16 @@
}, },
{ "@id": "urn:solid-server:default:WebAclReader" } { "@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" }
} }
] ]
} }

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

42
config/restrict-idp.json Normal file
View File

@ -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."
]
}
]
}

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -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<void> {
const { authorization } = headers;
if (authorization) {
throw new NotImplementedHttpError('Unexpected Authorization scheme.');
}
}
public async handle(): Promise<CredentialSet> {
return { [CredentialGroup.public]: {}};
}
}

View File

@ -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<CredentialSet> {
return { [CredentialGroup.public]: {}};
}
}

View File

@ -3,7 +3,7 @@ export * from './authentication/BearerWebIdExtractor';
export * from './authentication/Credentials'; export * from './authentication/Credentials';
export * from './authentication/CredentialsExtractor'; export * from './authentication/CredentialsExtractor';
export * from './authentication/DPoPWebIdExtractor'; export * from './authentication/DPoPWebIdExtractor';
export * from './authentication/EmptyCredentialsExtractor'; export * from './authentication/PublicCredentialsExtractor';
export * from './authentication/UnionCredentialsExtractor'; export * from './authentication/UnionCredentialsExtractor';
export * from './authentication/UnsecureConstantCredentialsExtractor'; export * from './authentication/UnsecureConstantCredentialsExtractor';
export * from './authentication/UnsecureWebIdExtractor'; export * from './authentication/UnsecureWebIdExtractor';

View File

@ -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<void> => { it('creates a pod with the given config.', async(): Promise<void> => {
const res = await fetch(`${baseUrl}idp/register`, { const res = await fetch(`${baseUrl}idp/register/`, {
method: 'POST', method: 'POST',
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings), 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<void> => { it('should not be able to create a pod with the same name.', async(): Promise<void> => {
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' }; 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', method: 'POST',
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
body: JSON.stringify(newSettings), body: JSON.stringify(newSettings),

View File

@ -96,7 +96,7 @@ describe('A Solid server with IDP', (): void => {
}); });
it('sends the form once to receive the registration triple.', async(): Promise<void> => { it('sends the form once to receive the registration triple.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register`, formBody); const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(400); expect(res.status).toBe(400);
registrationTriple = extractRegistrationTriple(await res.text(), webId); 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<void> => { it('sends the form again to successfully register.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register`, formBody); const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const text = await res.text(); const text = await res.text();
expect(text).toMatch(new RegExp(`your.WebID.*${webId}`, 'u')); expect(text).toMatch(new RegExp(`your.WebID.*${webId}`, 'u'));
@ -184,7 +184,7 @@ describe('A Solid server with IDP', (): void => {
let nextUrl: string; let nextUrl: string;
it('sends the corresponding email address through the form to get a mail.', async(): Promise<void> => { it('sends the corresponding email address through the form to get a mail.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/forgotpassword`, stringify({ email })); const res = await postForm(`${baseUrl}idp/forgotpassword/`, stringify({ email }));
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(load(await res.text())('form p').first().text().trim()) 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.'); .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<void> => { it('sends the form once to receive the registration triple.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register`, formBody); const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(400); expect(res.status).toBe(400);
registrationTriple = extractRegistrationTriple(await res.text(), webId2); 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<void> => { it('sends the form again to successfully register.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register`, formBody); const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const text = await res.text(); const text = await res.text();
expect(text).toMatch(new RegExp(`Your new Pod.*${baseUrl}${podName}/`, 'u')); 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<void> => { it('sends the form to create the WebID and register.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register`, formBody); const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const text = await res.text(); const text = await res.text();

View File

@ -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<void> => {
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
getTestConfigPath('restricted-idp.json'),
getDefaultVariables(port, baseUrl),
) as Record<string, any>;
({ app } = instances);
await app.start();
});
afterAll(async(): Promise<void> => {
await app.stop();
});
it('has ACL resources in the relevant containers.', async(): Promise<void> => {
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<void> => {
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<void> => {
// Only allow new WebID to register
const restrictedAcl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
<#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<void> => {
// 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'));
});
});

View File

@ -55,7 +55,7 @@ describe('A Solid server with setup', (): void => {
// Registration still possible // Registration still possible
const registerParams = { email, podName, password, confirmPassword: password, createWebId: true }; 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', method: 'POST',
headers: { accept: 'text/html', 'content-type': 'application/json' }, headers: { accept: 'text/html', 'content-type': 'application/json' },
body: JSON.stringify(registerParams), body: JSON.stringify(registerParams),
@ -83,7 +83,7 @@ describe('A Solid server with setup', (): void => {
// Root pod registration is never allowed // Root pod registration is never allowed
const registerParams = { email, podName, password, confirmPassword: password, createWebId: true, rootPod: true }; 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', method: 'POST',
headers: { accept: 'text/html', 'content-type': 'application/json' }, headers: { accept: 'text/html', 'content-type': 'application/json' },
body: JSON.stringify(registerParams), body: JSON.stringify(registerParams),

View File

@ -89,7 +89,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
describe('handling pods', (): void => { describe('handling pods', (): void => {
it('creates pods in a subdomain.', async(): Promise<void> => { it('creates pods in a subdomain.', async(): Promise<void> => {
const res = await fetch(`${baseUrl}idp/register`, { const res = await fetch(`${baseUrl}idp/register/`, {
method: 'POST', method: 'POST',
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings), 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<void> => { it('should not be able to create a pod with the same name.', async(): Promise<void> => {
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' }; 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', method: 'POST',
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
body: JSON.stringify(newSettings), body: JSON.stringify(newSettings),

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json",
"files-scs:config/http/static/default.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/handler/default.json",
"files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authentication/debug-auth-header.json",
"files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/authorization/webacl.json",

View File

@ -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" }
}
]
}
]
}

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/unsafe-no-check.json", "files-scs:config/identity/ownership/unsafe-no-check.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",
"files-scs:config/identity/pod/static.json", "files-scs:config/identity/pod/static.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/unsafe-no-check.json", "files-scs:config/identity/ownership/unsafe-no-check.json",

View File

@ -8,6 +8,7 @@
"files-scs:config/http/middleware/websockets.json", "files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.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/email/default.json",
"files-scs:config/identity/handler/default.json", "files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json", "files-scs:config/identity/ownership/token.json",

View File

@ -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<void> => {
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<void> => {
const headers = {};
const result = extractor.handleSafe({ headers } as HttpRequest);
await expect(result).resolves.toEqual({ [CredentialGroup.public]: {}});
});
});

View File

@ -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<void> => {
const headers = {};
const result = extractor.handleSafe({ headers } as HttpRequest);
await expect(result).resolves.toEqual({ [CredentialGroup.public]: {}});
});
});

View File

@ -12,6 +12,7 @@ const portNames = [
'Middleware', 'Middleware',
'PodCreation', 'PodCreation',
'RedisResourceLocker', 'RedisResourceLocker',
'RestrictedIdentity',
'ServerFetch', 'ServerFetch',
'SetupMemory', 'SetupMemory',
'SparqlStorage', 'SparqlStorage',