diff --git a/package-lock.json b/package-lock.json index 2882960..f45c9dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-logic", - "version": "6.0.1-0", + "version": "6.0.1-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-logic", - "version": "6.0.1-0", + "version": "6.0.1-1", "license": "MIT", "dependencies": { "@dokieli/web-access-control": "^1.0.0", diff --git a/package.json b/package.json index 6a6757d..feb0fed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-logic", - "version": "6.0.1-0", + "version": "6.0.1-1", "description": "Core business logic of SolidOS", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", diff --git a/src/acl/aclLogic.ts b/src/acl/aclLogic.ts index e07307b..a73f392 100644 --- a/src/acl/aclLogic.ts +++ b/src/acl/aclLogic.ts @@ -1,4 +1,4 @@ -import { applyPlan as applyAuthorizationPlan, findEffectiveACL, planGrant as planAuthorizationGrant, planPublicRead as planAuthorizationPublicRead, planRevoke as planAuthorizationRevoke, type AccessMode, type AccessSubject, type ACLContext, type Authorization, type PatchPlan } from '@dokieli/web-access-control' +import { applyPlan as applyAuthorizationPlan, Authenticated, findEffectiveACL, planGrant as planAuthorizationGrant, planPublicRead as planAuthorizationPublicRead, planRevoke as planAuthorizationRevoke, Public, type AccessMode, type AccessSubject, type ACLContext, type Authorization, type PatchPlan } from '@dokieli/web-access-control' import { graph, NamedNode, Namespace, serialize, sym } from 'rdflib' import type { AclLogic } from '../types' import { ns as namespace } from '../util/ns' @@ -15,8 +15,11 @@ import * as accessControlSubjects from './accessControlSubjects' // - Terms and constants: ACCESS_MODES, ACL, Authenticated, FOAF, modeFromIRI, modeIRI, namedNode, Public, quad, RDF_TYPE, variable export const ACCESS_ROLES = ['Owner', 'Editor', 'No Access', 'Viewer', 'Poster', 'Submitter'] as const +export const PUBLIC_ACCESS_ROLES = ['No Access', 'Viewer'] as const +export { Authenticated, Public } from '@dokieli/web-access-control' export type AccessRole = typeof ACCESS_ROLES[number] +export type PublicAccessRole = typeof PUBLIC_ACCESS_ROLES[number] export const ACL_LINK = sym( 'http://www.iana.org/assignments/link-relations/acl' @@ -33,6 +36,10 @@ export function createAclLogic(store): AclLogic { { label: 'Viewer', modes: ['Read'] }, { label: 'No Access', modes: [] } ] + const PUBLIC_ROLE_RULES: Array<{ label: PublicAccessRole, modes: AccessMode[] }> = [ + { label: 'Viewer', modes: ['Read'] }, + { label: 'No Access', modes: [] } + ] function roleFromModes (modes: Iterable): AccessRole { const modeSet = new Set(modes) @@ -45,6 +52,17 @@ export function createAclLogic(store): AclLogic { return [...(ACCESS_ROLE_RULES.find(rule => rule.label === role)?.modes ?? [])] } + function publicRoleFromModes (modes: Iterable): PublicAccessRole { + const modeSet = new Set(modes) + const matchingRule = PUBLIC_ROLE_RULES.find(rule => rule.label !== 'No Access' && rule.modes.every(mode => modeSet.has(mode))) + + return matchingRule?.label ?? 'No Access' + } + + function modesFromPublicRole (role: PublicAccessRole): AccessMode[] { + return [...(PUBLIC_ROLE_RULES.find(rule => rule.label === role)?.modes ?? [])] + } + function getFetch() { const fetcher = store.fetcher as { _fetch?: (url: string, init?: RequestInit) => Promise @@ -232,12 +250,17 @@ export function createAclLogic(store): AclLogic { findAccessGrants, roleFromModes, modesFromRole, + publicRoleFromModes, + modesFromPublicRole, planGrant, planRevoke, planPublicRead, applyPlan, classifyAccessControlSubject, setACLUserPublic, - genACLText + genACLText, + Authenticated, + Public, + PUBLIC_ACCESS_ROLES } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4dc0f9e..8cefa81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,8 @@ const authn = solidLogicSingleton.authn const authSession = solidLogicSingleton.authn.authSession const store = solidLogicSingleton.store -export { ACL_LINK, ACCESS_ROLES } from './acl/aclLogic' -export type { AccessRole } from './acl/aclLogic' +export { ACL_LINK, ACCESS_ROLES, PUBLIC_ACCESS_ROLES, Authenticated, Public } from './acl/aclLogic' +export type { AccessRole, PublicAccessRole } from './acl/aclLogic' export { offlineTestID, appContext } from './authn/authUtil' export { performServerSideLogout } from './authn/serverLogout' export { reloadOnIdentityReplaced } from './authSession/identityState' diff --git a/src/types.ts b/src/types.ts index eea8426..d5deb03 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ import type { SessionWithLegacyEvents } from './authSession/authSession' import type { ACLContext, AccessMode, AccessSubject, Authorization, PatchPlan } from '@dokieli/web-access-control' import { LiveStore, NamedNode, Statement } from 'rdflib' import type { AccessControlSubject } from './acl/accessControlSubjects' -import type { AccessRole } from './acl/aclLogic' +import type { AccessRole, PublicAccessRole } from './acl/aclLogic' export type AppDetails = { noun: string @@ -87,6 +87,8 @@ export interface AclLogic { classifyAccessControlSubject: (principle: string) => Promise, roleFromModes: (modes: Iterable) => AccessRole, modesFromRole: (role: AccessRole) => AccessMode[], + publicRoleFromModes: (modes: Iterable) => PublicAccessRole, + modesFromPublicRole: (role: PublicAccessRole) => AccessMode[], planGrant: (resourceURL: string | NamedNode, subject: AccessSubject, modes: AccessMode[]) => Promise, planRevoke: (resourceURL: string | NamedNode, subject: AccessSubject) => Promise, planPublicRead: (resourceURL: string | NamedNode, enabled: boolean) => Promise, @@ -103,6 +105,9 @@ export interface AclLogic { public?: [] } ) => string | undefined + Authenticated: AccessSubject + Public: AccessSubject + PUBLIC_ACCESS_ROLES: readonly PublicAccessRole[] } export type { ACLContext, AccessMode, AccessSubject, Authorization, PatchPlan } diff --git a/test/publicAclApi.test.ts b/test/publicAclApi.test.ts index a2f9926..984864b 100644 --- a/test/publicAclApi.test.ts +++ b/test/publicAclApi.test.ts @@ -1,6 +1,9 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { ACCESS_ROLES, + Authenticated, + PUBLIC_ACCESS_ROLES, + Public, solidLogicSingleton, type ACLContext, type AccessControlSubjectKind, @@ -8,15 +11,22 @@ import { type AccessRole, type AccessSubject, type Authorization, - type PatchPlan + type PatchPlan, + type PublicAccessRole } from '../src' describe('public ACL API', () => { it('exports the role list and exposes mapping through the singleton', () => { expect(ACCESS_ROLES).toEqual(['Owner', 'Editor', 'No Access', 'Viewer', 'Poster', 'Submitter']) + expect(PUBLIC_ACCESS_ROLES).toEqual(['No Access', 'Viewer']) + expect(Authenticated).toMatchObject({ type: 'agentClass', iri: 'http://www.w3.org/ns/auth/acl#AuthenticatedAgent' }) + expect(Public).toMatchObject({ type: 'agentClass', iri: 'http://xmlns.com/foaf/0.1/Agent' }) for (const role of ACCESS_ROLES) { expect(solidLogicSingleton.acl.roleFromModes(solidLogicSingleton.acl.modesFromRole(role))).toBe(role) } + for (const role of PUBLIC_ACCESS_ROLES) { + expect(solidLogicSingleton.acl.publicRoleFromModes(solidLogicSingleton.acl.modesFromPublicRole(role))).toBe(role) + } }) it('exports the types used by access-control consumers', () => { @@ -27,6 +37,8 @@ describe('public ACL API', () => { expectTypeOf(solidLogicSingleton.acl.planGrant).toEqualTypeOf< (resource: string | import('rdflib').NamedNode, subject: AccessSubject, modes: AccessMode[]) => Promise >() + expectTypeOf(solidLogicSingleton.acl.publicRoleFromModes).toEqualTypeOf<(modes: Iterable) => PublicAccessRole>() + expectTypeOf(solidLogicSingleton.acl.modesFromPublicRole).toEqualTypeOf<(role: PublicAccessRole) => AccessMode[]>() expectTypeOf().toEqualTypeOf<'agent' | 'agentGroup' | 'agentClass' | 'origin'>() }) }) diff --git a/test/resource/resourceLogic.test.ts b/test/resource/resourceLogic.test.ts index 0819e83..661e948 100644 --- a/test/resource/resourceLogic.test.ts +++ b/test/resource/resourceLogic.test.ts @@ -37,8 +37,13 @@ function buildResourceLogic(overrides: { findAclDocUrl: vi.fn(overrides.findAclDocUrl ?? (async () => undefined)), roleFromModes: vi.fn(), modesFromRole: vi.fn(), + publicRoleFromModes: vi.fn(), + modesFromPublicRole: vi.fn(), setACLUserPublic: vi.fn(), - genACLText: vi.fn() + genACLText: vi.fn(), + Authenticated: { type: 'agentClass', iri: 'http://www.w3.org/ns/auth/acl#AuthenticatedAgent' }, + Public: { type: 'agentClass', iri: 'http://xmlns.com/foaf/0.1/Agent' }, + PUBLIC_ACCESS_ROLES: ['No Access', 'Viewer'] } as any, { createContainer: vi.fn(async () => undefined), diff --git a/test/resourceLogic.test.ts b/test/resourceLogic.test.ts index 642f6e1..63d0277 100644 --- a/test/resourceLogic.test.ts +++ b/test/resourceLogic.test.ts @@ -40,12 +40,17 @@ describe('resourceLogic', () => { classifyAccessControlSubject: vi.fn().mockResolvedValue(undefined), roleFromModes: vi.fn(), modesFromRole: vi.fn(), + publicRoleFromModes: vi.fn(), + modesFromPublicRole: vi.fn(), planGrant: vi.fn(), planRevoke: vi.fn(), planPublicRead: vi.fn(), applyPlan: vi.fn(), setACLUserPublic: vi.fn(), - genACLText: vi.fn() + genACLText: vi.fn(), + Authenticated: { type: 'agentClass', iri: 'http://www.w3.org/ns/auth/acl#AuthenticatedAgent' }, + Public: { type: 'agentClass', iri: 'http://xmlns.com/foaf/0.1/Agent' }, + PUBLIC_ACCESS_ROLES: ['No Access', 'Viewer'] } containerLogic = { createContainer: vi.fn(),