Skip to content

Commit

Permalink
Permitiendo crear tokens para la API
Browse files Browse the repository at this point in the history
Este cambio permite crear hasta 5 API tokens. También permite revocar
tokens.

Part of: omegaup#5242
  • Loading branch information
lhchavez committed Mar 22, 2021
1 parent 4a4703c commit a20d41a
Show file tree
Hide file tree
Showing 23 changed files with 392 additions and 15 deletions.
3 changes: 3 additions & 0 deletions frontend/database/174_add_apitokens_name_uniqueness.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- API_Tokens
ALTER TABLE `API_Tokens`
ADD CONSTRAINT UNIQUE `user_name` (`user_id`, `name`);
1 change: 1 addition & 0 deletions frontend/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ CREATE TABLE `API_Tokens` (
`use_count` int NOT NULL DEFAULT '0' COMMENT 'Número de usos desde la última hora',
PRIMARY KEY (`apitoken_id`),
UNIQUE KEY `token` (`token`),
UNIQUE KEY `user_name` (`user_id`,`name`),
KEY `user_id` (`user_id`),
CONSTRAINT `fk_atu_user_id` FOREIGN KEY (`user_id`) REFERENCES `Users` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Tokens para el API';
Expand Down
62 changes: 62 additions & 0 deletions frontend/server/src/Controllers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
- [`/api/user/coderOfTheMonthList/`](#apiusercoderofthemonthlist)
- [`/api/user/contestStats/`](#apiuserconteststats)
- [`/api/user/create/`](#apiusercreate)
- [`/api/user/createAPIToken/`](#apiusercreateapitoken)
- [`/api/user/extraInformation/`](#apiuserextrainformation)
- [`/api/user/generateGitToken/`](#apiusergenerategittoken)
- [`/api/user/generateOmiUsers/`](#apiusergenerateomiusers)
Expand All @@ -227,6 +228,7 @@
- [`/api/user/removeExperiment/`](#apiuserremoveexperiment)
- [`/api/user/removeGroup/`](#apiuserremovegroup)
- [`/api/user/removeRole/`](#apiuserremoverole)
- [`/api/user/revokeAPIToken/`](#apiuserrevokeapitoken)
- [`/api/user/selectCoderOfTheMonth/`](#apiuserselectcoderofthemonth)
- [`/api/user/stats/`](#apiuserstats)
- [`/api/user/statusVerified/`](#apiuserstatusverified)
Expand Down Expand Up @@ -4077,6 +4079,50 @@ Entry point for Create a User API
| ---------- | -------- |
| `username` | `string` |

## `/api/user/createAPIToken/`

### Description

Creates a new API token associated with the user.

This token can be used to authenticate against the API in other calls
through the [HTTP `Authorization`
header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
in the request:

```
Authorization: token 92d8c5a0eceef3c05f4149fc04b62bb2cd50d9c6
```

The following alternative syntax allows to specify an associated
identity:

```
Authorization: token Credential=92d8c5a0eceef3c05f4149fc04b62bb2cd50d9c6,Username=groupname:username
```

There is a limit of 1000 requests that can be done every hour, after
which point all requests will fail with [HTTP 429 Too Many
Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429).
The `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
`X-RateLimit-Reset` response headers will be set whenever an API token
is used and will contain useful information about the limit to the
caller.

There is a limit of 5 API tokens that each user can have.

### Parameters

| Name | Type | Description |
| ------ | -------- | -------------------------------------------------------------------- |
| `name` | `string` | A non-empty alphanumeric string. May contain underscores and dashes. |

### Returns

| Name | Type |
| ------- | -------- |
| `token` | `string` |

## `/api/user/extraInformation/`

### Description
Expand Down Expand Up @@ -4370,6 +4416,22 @@ Removes the role from the user.

_Nothing_

## `/api/user/revokeAPIToken/`

### Description

Revokes an API token associated with the user.

### Parameters

| Name | Type | Description |
| ------ | -------- | -------------------------------------------------------------------- |
| `name` | `string` | A non-empty alphanumeric string. May contain underscores and dashes. |

### Returns

_Nothing_

## `/api/user/selectCoderOfTheMonth/`

### Description
Expand Down
94 changes: 94 additions & 0 deletions frontend/server/src/Controllers/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4102,6 +4102,100 @@ public static function getLoginDetailsForTypeScript(\OmegaUp\Request $r) {
}
return $response;
}

/**
* Creates a new API token associated with the user.
*
* This token can be used to authenticate against the API in other calls
* through the [HTTP `Authorization`
* header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
* in the request:
*
* ```
* Authorization: token 92d8c5a0eceef3c05f4149fc04b62bb2cd50d9c6
* ```
*
* The following alternative syntax allows to specify an associated
* identity:
*
* ```
* Authorization: token Credential=92d8c5a0eceef3c05f4149fc04b62bb2cd50d9c6,Username=groupname:username
* ```
*
* There is a limit of 1000 requests that can be done every hour, after
* which point all requests will fail with [HTTP 429 Too Many
* Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429).
* The `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
* `X-RateLimit-Reset` response headers will be set whenever an API token
* is used and will contain useful information about the limit to the
* caller.
*
* There is a limit of 5 API tokens that each user can have.
*
* @return array{token: string}
*
* @omegaup-request-param string $name A non-empty alphanumeric string. May contain underscores and dashes.
*/
public static function apiCreateAPIToken(\OmegaUp\Request $r) {
\OmegaUp\Controllers\Controller::ensureNotInLockdown();
$r->ensureMainUserIdentity();

$name = $r->ensureString(
'name',
fn (string $name) => preg_match('/^[a-zA-Z0-9_-]+$/', $name) === 1,
);
$token = \OmegaUp\SecurityTools::randomHexString(40);
$apiToken = new \OmegaUp\DAO\VO\APITokens([
'user_id' => $r->user->user_id,
'token' => $token,
'name' => $name,
]);

try {
\OmegaUp\DAO\DAO::transBegin();

\OmegaUp\DAO\APITokens::create($apiToken);
if (\OmegaUp\DAO\APITokens::getCountByUser($r->user->user_id) > 5) {
throw new \OmegaUp\Exceptions\DuplicatedEntryInDatabaseException(
'apiTokenLimitExceeded'
);
}

\OmegaUp\DAO\DAO::transEnd();
return [
'token' => $token,
];
} catch (\Exception $e) {
\OmegaUp\DAO\DAO::transRollback();
if (\OmegaUp\DAO\DAO::isDuplicateEntryException($e)) {
throw new \OmegaUp\Exceptions\DuplicatedEntryInDatabaseException(
'apiTokenNameAlreadyInUse',
$e
);
}
throw $e;
}
}

/**
* Revokes an API token associated with the user.
*
* @return array{status: string}
*
* @omegaup-request-param string $name A non-empty alphanumeric string. May contain underscores and dashes.
*/
public static function apiRevokeAPIToken(\OmegaUp\Request $r) {
\OmegaUp\Controllers\Controller::ensureNotInLockdown();
$r->ensureMainUserIdentity();

$name = $r->ensureString(
'name',
fn (string $name) => preg_match('/^[a-zA-Z0-9_-]+$/', $name) === 1,
);

\OmegaUp\DAO\APITokens::deleteByName($r->user->user_id, $name);
return ['status' => 'ok'];
}
}

\OmegaUp\Controllers\User::$urlHelper = new \OmegaUp\UrlHelper();
30 changes: 30 additions & 0 deletions frontend/server/src/DAO/APITokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,34 @@ public static function updateUsage(
throw $e;
}
}

public static function getCountByUser(int $userId): int {
/** @var int */
return \OmegaUp\MySQLConnection::getInstance()->GetOne(
'
SELECT
COUNT(*)
FROM
`API_Tokens` at
WHERE
at.user_id = ?;
',
[$userId],
);
}

public static function deleteByName(int $userId, string $name): void {
\OmegaUp\MySQLConnection::getInstance()->Execute(
'
DELETE FROM
`API_Tokens`
WHERE
user_id = ? AND name = ?;
',
[$userId, $name],
);
if (\OmegaUp\MySQLConnection::getInstance()->Affected_Rows() == 0) {
throw new \OmegaUp\Exceptions\NotFoundException('recordNotFound');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class RateLimitExceededException extends \OmegaUp\Exceptions\ApiException {
public function __construct(
string $message = 'rateLimitExceeded',
string $message = 'apiTokenRateLimitExceeded',
?\Exception $previous = null
) {
parent::__construct(
Expand Down
2 changes: 1 addition & 1 deletion frontend/server/src/Psalm/TranslationStringChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ class TranslationStringChecker implements
* A list of messages that are present in the base exception classes.
*/
const EXCEPTION_MESSAGES = [
'apiTokenRateLimitExceeded',
'csrfException',
'emailNotVerified',
'errorWhileSendingMail',
'generalError',
'loginRequired',
'problemDeployerFailed',
'rateLimitExceeded',
'resourceNotFound',
'unableToVerifyCaptcha',
'userNotAllowed',
Expand Down
4 changes: 3 additions & 1 deletion frontend/templates/en.lang
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ aliasAlreadyInUse = "Alias \"%(alias)\" already exists. Please choose a differen
aliasInUse = "alias already exists. Please choose a different alias."
allowedSolutionsLimitReached = "You have reached the limit of allowed solutions"
apiNotFound = "Requested API endpoint not found."
apiTokenLimitExceeded = "Maximum number of API tokens exceeded."
apiTokenNameAlreadyInUse = "API token names must be unique"
apiTokenRateLimitExceeded = "Maximum number of API requests exceeded. Please try again later."
arenaClarificationCreate = "Type your clarification here:"
arenaClarificationCreateMaxLength = "Type your clarification here (Max. 200 characters, do not paste your entire code)."
arenaClarificationMaxLength = "Max. 200 characters, do not paste your entire code."
Expand Down Expand Up @@ -1068,7 +1071,6 @@ qualityNominationType = "Type"
rankRangeHeader = "Top %(lowCount) to %(highCount) users with the most points"
rankScore = "Score"
rankSolved = "Solved problems"
rateLimitExceeded = "Maximum number of API requests exceeded. Please try again later."
recordNotFound = "Record not found."
registerForContest = "Register for contest"
registerForContestChallenges = "Challenge"
Expand Down
4 changes: 3 additions & 1 deletion frontend/templates/es.lang
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ aliasAlreadyInUse = "El alias \"%(alias)\" ya está siendo usado. Por favor elig
aliasInUse = "El alias (título corto) ya está siendo usado. Por favor elige un alias distinto."
allowedSolutionsLimitReached = "Has alcanzado el límite de soluciones permitidas"
apiNotFound = "El API solicitada no fue encontrada."
apiTokenLimitExceeded = "Número máximo de tokens de API excedido."
apiTokenNameAlreadyInUse = "Los nombres de los tokens deben ser únicos"
apiTokenRateLimitExceeded = "Límite de peticiones al API excedido. Por favor intenta de nuevo más tarde."
arenaClarificationCreate = "Escribe tu pregunta aquí:"
arenaClarificationCreateMaxLength = "Escribe tu pregunta aquí (Máximo 200 caracteres, no incluyas tu código completo)."
arenaClarificationMaxLength = "Máximo 200 caracteres, no incluyas tu código completo."
Expand Down Expand Up @@ -1068,7 +1071,6 @@ qualityNominationType = "Tipo"
rankRangeHeader = "Los mejores %(lowCount) a %(highCount) usuarios con más puntaje"
rankScore = "Score"
rankSolved = "Problemas"
rateLimitExceeded = "Límite de peticiones al API excedido. Por favor intenta de nuevo más tarde."
recordNotFound = "Registro no encontrado."
registerForContest = "Registrarse en el concurso"
registerForContestChallenges = "Reto"
Expand Down
4 changes: 3 additions & 1 deletion frontend/templates/pseudo.lang
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ aliasAlreadyInUse = "(A1ia5 \"%(alias)\" a1r3ady 3xi575. P13a53 ch0053 a diff3r3
aliasInUse = "(a1ia5 a1r3ady 3xi575. P13a53 ch0053 a diff3r3n7 a1ia5.)"
allowedSolutionsLimitReached = "(Y0u hav3 r3ach3d 7h3 1imi7 0f a110w3d 501u7i0n5)"
apiNotFound = "(R3qu3573d API 3ndp0in7 n07 f0und.)"
apiTokenLimitExceeded = "(Maximum numb3r 0f API 70k3n5 3xc33d3d.)"
apiTokenNameAlreadyInUse = "(API 70k3n nam35 mu57 b3 uniqu3)"
apiTokenRateLimitExceeded = "(Maximum numb3r 0f API r3qu3575 3xc33d3d. P13a53 7ry again 1a73r.)"
arenaClarificationCreate = "(Typ3 y0ur c1arifica7i0n h3r3:)"
arenaClarificationCreateMaxLength = "(Typ3 y0ur c1arifica7i0n h3r3 (Max. 200 charac73r5, d0 n07 pa573 y0ur 3n7ir3 c0d3).)"
arenaClarificationMaxLength = "(Max. 200 charac73r5, d0 n07 pa573 y0ur 3n7ir3 c0d3.)"
Expand Down Expand Up @@ -1068,7 +1071,6 @@ qualityNominationType = "(Typ3)"
rankRangeHeader = "(T0p %(lowCount) 70 %(highCount) u53r5 wi7h 7h3 m057 p0in75)"
rankScore = "(Sc0r3)"
rankSolved = "(S01v3d pr0b13m5)"
rateLimitExceeded = "(Maximum numb3r 0f API r3qu3575 3xc33d3d. P13a53 7ry again 1a73r.)"
recordNotFound = "(R3c0rd n07 f0und.)"
registerForContest = "(R3gi573r f0r c0n7357)"
registerForContestChallenges = "(Cha113ng3)"
Expand Down
4 changes: 3 additions & 1 deletion frontend/templates/pt.lang
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ aliasAlreadyInUse = "Alias \"%(alias)\" já existe. Por favor, escolha um nome d
aliasInUse = "Alias já existe. Por favor, escolha um alias diferente."
allowedSolutionsLimitReached = "Você tem alcançado o limite de soluçoes permitidas"
apiNotFound = "Endpoint API solicitada não foi encontrado."
apiTokenLimitExceeded = "Número máximo de tokens da API excedido."
apiTokenNameAlreadyInUse = "Os nomes de tokens da API devem ser exclusivos"
apiTokenRateLimitExceeded = "Número máximo de solicitações da API excedido. Por favor, tente novamente mais tarde."
arenaClarificationCreate = "Digite sua pergunta aqui"
arenaClarificationCreateMaxLength = "Digite sua pergunta aqui (no máximo 200 caracteres, não incluir o seu código completo)"
arenaClarificationMaxLength = "No máximo 200 caracteres, não incluir o seu código completo."
Expand Down Expand Up @@ -1068,7 +1071,6 @@ qualityNominationType = "Tipo"
rankRangeHeader = "Os melhores %(lowCount) a %(highCount) usuários com maior score obtido"
rankScore = "Pontuação"
rankSolved = "Problemas"
rateLimitExceeded = "Número máximo de solicitações de API excedido. Por favor, tente novamente mais tarde."
recordNotFound = "Registro não foi encontrado."
registerForContest = "cadastre-se para o concurso"
registerForContestChallenges = "Desafio"
Expand Down
1 change: 1 addition & 0 deletions frontend/tests/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ private static function cleanupDB(): void {
'ACLs',
'Assignments',
'Auth_Tokens',
'API_Tokens',
'Clarifications',
'Coder_Of_The_Month',
'Contest_Log',
Expand Down
Loading

0 comments on commit a20d41a

Please sign in to comment.