diff --git a/openapi.yaml b/openapi.yaml index dac1a6c..120a591 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -57,31 +57,33 @@ tags: [ { name: Tokens, description: "Tokens enable authentication with the Prelude server. Using the token scopes, - you can control what endpoints the API token can be used for. - - - The available token scopes are: - - | Scope | Description | Recommended For | - - |-------------------|----------------------------------------------------------------------------------------------------------|-----------------| - - | `library:read` | Read-only access to Artists, Albums, and Tracks. | everyone | - - | `library:write` | Write/modify access to Artists, Albums, and Tracks. Allows the user to upload/delete audio files. | admin | - - | `tokens:read:self`| Read-only access to your Tokens. | everyone | - - | `tokens:write:self`| Write/modify access to your Tokens. | everyone | - - | `tokens:read:all` | Read-only access to everyone's Tokens. | admin | - - | `tokens:write:all`| Write/modify access to everyone's Tokens. | admin | - - | `users:read` | Read-only access to Users. | admin | - - | `users:write` | Write/modify access to Users. | admin | - " +you can control what endpoints the API token can be used for. + + +The available token scopes are: + +| Scope | Description | Recommended For | + +|---------------------|---------------------------------------------------------------------------------------------------|-----------------| + +| `library:read` | Read-only access to Artists, Albums, and Tracks. | everyone | + +| `library:write` | Write/modify access to Artists, Albums, and Tracks. Allows the user to upload/delete audio files. | admin | + +| `tokens:read:self` | Read-only access to your Tokens. | everyone | + +| `tokens:write:self` | Write/modify access to your Tokens. | everyone | + +| `tokens:read:all` | Read-only access to everyone's Tokens. | admin | + +| `tokens:write:all` | Write/modify access to everyone's Tokens. | admin | + +| `users:read` | Read-only access to Users. | admin | + +| `users:write` | Write/modify access to Users. | admin | + +| `admin` | Grants full administrative access, including future API scopes. | admin | +" } ] diff --git a/src/Authorisation.ts b/src/Authorisation.ts index 9eb6d3d..197488a 100644 --- a/src/Authorisation.ts +++ b/src/Authorisation.ts @@ -8,12 +8,16 @@ import ErrorResponse from "./response/ErrorResponse.js"; export default class Authorisation { public constructor( public readonly user: User, - public readonly scopes: Readonly> + private readonly scopes: Readonly> ) { } public require(scope: Token.Scope): void { - if (!this.scopes.has(scope)) throw new ThrowableResponse(Authorisation.FORBIDDEN); + if (!this.has(scope)) throw new ThrowableResponse(Authorisation.FORBIDDEN); + } + + public has(scope: Token.Scope): boolean { + return this.scopes.has(scope) || this.scopes.has(Token.Scope.ADMIN); } public static fromToken(secret: Token.Secret, library: Library): Authorisation | null { diff --git a/src/index.ts b/src/index.ts index a916875..15fc810 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,7 +60,7 @@ if (library.repositories.users.list({limit: 0, offset: 0}).total === 0) { library.repositories.users.save(new User( User.ID.random(), "admin", - new Set(Object.values(Token.Scope)), // all scopes + new Set([Token.Scope.ADMIN]), await Password.hash(password), false )); diff --git a/src/resource/Token.ts b/src/resource/Token.ts index 6631e20..bd5b5a4 100644 --- a/src/resource/Token.ts +++ b/src/resource/Token.ts @@ -162,7 +162,13 @@ namespace Token { * * Recommended for: admin */ - USERS_WRITE = "users:write" + USERS_WRITE = "users:write", + + /** + * Grants full administrative access. + * This scope acts as a wildcard, providing full access even if new API scopes are introduced in future updates. + */ + ADMIN = "admin" } export class Repository extends Repository_ { @@ -266,11 +272,11 @@ namespace Token { token(req: ApiRequest, repo: Token.Repository, id: string): Token { if (req.auth === null) throw new ThrowableResponse(Authorisation.UNAUTHORISED); - if (!req.auth.scopes.has(Token.Scope.TOKENS_READ_ALL) && !req.auth.scopes.has(Token.Scope.TOKENS_READ_SELF)) + if (!req.auth.has(Token.Scope.TOKENS_READ_ALL) && !req.auth.has(Token.Scope.TOKENS_READ_SELF)) throw new ThrowableResponse(Authorisation.FORBIDDEN); const token = repo.get(new Token.ID(id)); - if (token === null || (!req.auth.scopes.has(Token.Scope.TOKENS_READ_ALL) && !token.user.equals(req.auth.user.id))) + if (token === null || (!req.auth.has(Token.Scope.TOKENS_READ_ALL) && !token.user.equals(req.auth.user.id))) throw new ThrowableResponse(Token.Controller.notFound()); return token; } @@ -281,7 +287,7 @@ namespace Token { */ private validateScopes(auth: Authorisation, scopes: Set) { for (const scope of scopes) - if (!auth.scopes.has(scope)) throw new ThrowableResponse(new FieldErrorResponse({scopes: `You don't have permission to grant scope ${scope} in the current authorisation context.`}, {}, 403)); + if (!auth.has(scope)) throw new ThrowableResponse(new FieldErrorResponse({scopes: `You don't have permission to grant scope ${scope} in the current authorisation context.`}, {}, 403)); } public static notFound() { @@ -290,9 +296,9 @@ namespace Token { protected override list(req: ApiRequest): ApiResponse { if (req.auth === null) return Authorisation.UNAUTHORISED; - if (!req.auth.scopes.has(Token.Scope.TOKENS_READ_ALL) && !req.auth.scopes.has(Token.Scope.TOKENS_READ_SELF)) return Authorisation.FORBIDDEN; + if (!req.auth.has(Token.Scope.TOKENS_READ_ALL) && !req.auth.has(Token.Scope.TOKENS_READ_SELF)) return Authorisation.FORBIDDEN; const limit = req.limit(); - const tokens = req.auth.scopes.has(Token.Scope.TOKENS_READ_ALL) + const tokens = req.auth.has(Token.Scope.TOKENS_READ_ALL) ? (req.url.searchParams.has("user") ? this.library.repositories.tokens.listOfUser(new User.ID(req.url.searchParams.get("user")!), limit) : (req.url.searchParams.has("all") @@ -306,9 +312,9 @@ namespace Token { protected override create(req: ApiRequest): ApiResponse { if (req.auth === null) return Authorisation.UNAUTHORISED; - const userId = req.auth.scopes.has(Token.Scope.TOKENS_WRITE_SELF) + const userId = req.auth.has(Token.Scope.TOKENS_WRITE_SELF) ? req.auth.user.id - : (req.auth.scopes.has(Token.Scope.TOKENS_WRITE_ALL) + : (req.auth.has(Token.Scope.TOKENS_WRITE_ALL) ? this.extract.user(req.body) ?? req.auth.user.id : null); if (userId === null) return Authorisation.FORBIDDEN; @@ -328,8 +334,8 @@ namespace Token { protected override deleteAll(req: ApiRequest): ApiResponse { if (req.auth === null) return Authorisation.UNAUTHORISED; - if (req.auth.scopes.has(Token.Scope.TOKENS_WRITE_ALL)) this.library.repositories.tokens.deleteAll(); - else if (req.auth.scopes.has(Token.Scope.TOKENS_WRITE_SELF)) this.library.repositories.tokens.deleteOfUser(req.auth.user.id); + if (req.auth.has(Token.Scope.TOKENS_WRITE_ALL)) this.library.repositories.tokens.deleteAll(); + else if (req.auth.has(Token.Scope.TOKENS_WRITE_SELF)) this.library.repositories.tokens.deleteOfUser(req.auth.user.id); else return Authorisation.FORBIDDEN; return new EmptyReponse(); }