Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wildcard admin scope #35

Merged
merged 1 commit into from
Aug 1, 2024
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
52 changes: 27 additions & 25 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
"
}

]
Expand Down
8 changes: 6 additions & 2 deletions src/Authorisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import ErrorResponse from "./response/ErrorResponse.js";
export default class Authorisation {
public constructor(
public readonly user: User,
public readonly scopes: Readonly<Set<Token.Scope>>
private readonly scopes: Readonly<Set<Token.Scope>>
) {
}

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 {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
));
Expand Down
26 changes: 16 additions & 10 deletions src/resource/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_<Token> {
Expand Down Expand Up @@ -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;
}
Expand All @@ -281,7 +287,7 @@ namespace Token {
*/
private validateScopes(auth: Authorisation, scopes: Set<Token.Scope>) {
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() {
Expand All @@ -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")
Expand 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;
Expand All @@ -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();
}
Expand Down