Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
}
}

.access-grants-image--group {
.access-grants-image--agentGroup {
background: var(--solid-ui-color-blue-900, #083575);
}

Expand All @@ -198,6 +198,10 @@
background: var(--solid-ui-color-lavender-300, #e6dcff);
}

.access-grants-image--origin {
background: var(--solid-ui-color-green-700, #15803D);
}

.access-grants-image--unknown {
background: var(--solid-ui-color-gray-300, #D1D5DB);
}
Expand Down
72 changes: 55 additions & 17 deletions src/components/access-control-modal/AccessControlModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import '@/components/combobox-option'
import styles from './AccessControlModal.styles.css'
import type { ComboboxChangeEvent } from '@/components/combobox'
import type { AccessChange, AccessControlBadge, AccessGrantEntry, AuthorizationSubjectSet, ChangedAccessGrant, PendingAccessGrant } from './types'
import { dedupeByKey, dedupeComboboxOptions, isHttpUri, type StringComboboxOptionData } from './helpers'
import { dedupeByKey, dedupeComboboxOptions, isAbsoluteHttpUri, isOriginUrl, type StringComboboxOptionData } from './helpers'

function isDefined<T> (value: T | undefined | null): value is T {
return value !== undefined && value !== null
Expand Down Expand Up @@ -188,7 +188,7 @@ export default class AccessControlModal extends WebComponent {
const agentGroup = authorization.agentGroup[0]
if (agentGroup) {
return {
kind: 'group' as const,
kind: 'agentGroup' as const,
text: this.getInitials(resolvedSubjectLabel, 1)
}
}
Expand All @@ -203,6 +203,14 @@ export default class AccessControlModal extends WebComponent {
}
}

const origin = authorization.origin[0]
if (origin) {
return {
kind: 'origin' as const,
text: 'O'
}
}

return {
kind: 'unknown' as const,
text: '?'
Expand All @@ -228,6 +236,7 @@ export default class AccessControlModal extends WebComponent {
...authorization.agent,
...authorization.agentGroup,
...authorization.agentClass,
...authorization.origin,
]
}

Expand Down Expand Up @@ -753,7 +762,8 @@ export default class AccessControlModal extends WebComponent {
const subjectSets: AuthorizationSubjectSet[] = [
{ type: 'agent', iris: authorization.agent },
{ type: 'agentGroup', iris: authorization.agentGroup },
{ type: 'agentClass', iris: authorization.agentClass }
{ type: 'agentClass', iris: authorization.agentClass },
{ type: 'origin', iris: authorization.origin }
]

return subjectSets.flatMap(({ type, iris }) => iris.map(iri => ({ type, iri })))
Expand Down Expand Up @@ -846,8 +856,11 @@ export default class AccessControlModal extends WebComponent {
): Promise<PendingAccessGrantCreationResult> {
try {
const normalizedPrinciple = principle.trim()
const resolvedPrinciple = await solidLogicSingleton.acl.classifyAccessControlSubject(normalizedPrinciple)
const fallbackKind = this.isHttpUri(principle) ? 'agent' : undefined
const normalizedOrigin = this.normalizeOriginInput(normalizedPrinciple)
const resolvedPrinciple = normalizedOrigin
? { kind: 'origin' as const, subjectValue: normalizedOrigin }
: await solidLogicSingleton.acl.classifyAccessControlSubject(normalizedPrinciple)
const fallbackKind = isAbsoluteHttpUri(principle) ? 'agent' : undefined
const subjectType = resolvedPrinciple?.kind ?? fallbackKind
const subjectValue = resolvedPrinciple?.subjectValue ?? normalizedPrinciple

Expand All @@ -858,13 +871,6 @@ export default class AccessControlModal extends WebComponent {
}
}

if (subjectType === 'origin') {
return {
principle,
error: 'Origin access grants are not supported yet'
}
}

const label = preferredLabel ?? await this.resolvePendingAccessGrantLabel(subjectValue, principle)

return {
Expand Down Expand Up @@ -945,14 +951,50 @@ export default class AccessControlModal extends WebComponent {
}

private async getPreferredAccessPrincipleOptions (query: string): Promise<ComboboxOptionData[]> {
if (!isHttpUri(query)) {
const normalizedOrigin = this.normalizeOriginInput(query)
if (normalizedOrigin) {
return [{
label: `Use origin ${normalizedOrigin}`,
value: normalizedOrigin
}]
}

if (!isAbsoluteHttpUri(query)) {
return []
}

if (isOriginUrl(query)) {
return [{
label: `Use origin ${new URL(query).origin}`,
value: new URL(query).origin
}]
}

const urlOption = await this.createUrlOption(query)
return dedupeComboboxOptions(urlOption ? [urlOption] : [])
}

private normalizeOriginInput (value: string): string | undefined {
const trimmedValue = value.trim()
if (!trimmedValue || isAbsoluteHttpUri(trimmedValue)) {
return undefined
}

try {
const parsed = new URL(`https://${trimmedValue}`)
const isHttpOrigin = parsed.protocol === 'http:' || parsed.protocol === 'https:'
const hasOnlyOriginParts = parsed.pathname === '/' && !parsed.search && !parsed.hash
const isRecognizedHost = parsed.hostname.includes('.') ||
parsed.hostname.includes(':')

return isHttpOrigin && hasOnlyOriginParts && isRecognizedHost && !parsed.username && !parsed.password
? parsed.origin
: undefined
} catch {
return undefined
}
}

private async createUrlOption (uri: string): Promise<StringComboboxOptionData | undefined> {
try {
await solidLogicSingleton.store.fetcher.load(sym(uri).doc())
Expand Down Expand Up @@ -1003,10 +1045,6 @@ export default class AccessControlModal extends WebComponent {
return nothing
}

private isHttpUri (value: string): boolean {
return value.startsWith('http://') || value.startsWith('https://')
}

private async onCopyLinkClick (event: Event) {
event.preventDefault()

Expand Down
22 changes: 20 additions & 2 deletions src/components/access-control-modal/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@ import type { ComboboxOptionData } from '@/components/combobox'

export type StringComboboxOptionData = ComboboxOptionData & { value: string }

export function isHttpUri (possibleUri: string): boolean {
return possibleUri.startsWith('http://') || possibleUri.startsWith('https://')
export function isAbsoluteHttpUri (value: string): boolean {
try {
const url = new URL(value)
return (url.protocol === 'http:' || url.protocol === 'https:') && Boolean(url.hostname)
} catch {
return false
}
}

export function isOriginUrl (value: string): boolean {
if (!isAbsoluteHttpUri(value)) {
return false
}

const url = new URL(value)
return url.pathname === '/' &&
!url.search &&
!url.hash &&
!url.username &&
!url.password
}

export function dedupeByKey<T> (items: T[], getKey: (item: T) => string | undefined): T[] {
Expand Down
7 changes: 3 additions & 4 deletions src/components/access-control-modal/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { AccessControlSubjectKind, AccessRole, AccessSubject, Authorization } from 'solid-logic'

export type AccessControlBadgeKind = 'agent' | 'group' | 'agentClass' | 'unknown'
export type PendingAccessSubjectKind = Exclude<AccessControlSubjectKind, 'origin'>
export type AccessControlBadgeKind = AccessControlSubjectKind | 'unknown'

export type AccessControlBadge = {
kind: AccessControlBadgeKind
Expand All @@ -11,7 +10,7 @@ export type AccessControlBadge = {

export type PendingAccessGrant = {
id?: string
subjectType: PendingAccessSubjectKind
subjectType: AccessControlSubjectKind
subjectValue: string
role: AccessRole
label: string
Expand All @@ -32,7 +31,7 @@ export type ChangedAccessGrant = {
}

export type AuthorizationSubjectSet = {
type: PendingAccessSubjectKind
type: AccessControlSubjectKind
iris: string[]
}

Expand Down
Loading
Loading