Skip to content

feat: personal access tokens (PATs) #1924

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

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open

feat: personal access tokens (PATs) #1924

wants to merge 33 commits into from

Conversation

nas-tabchiche
Copy link
Contributor

@nas-tabchiche nas-tabchiche commented Apr 28, 2025

  • limit tokens per user to 5
  • implement CRUD operations for PATs
  • fetch user's auth tokens
  • write AuthTokenCreateSchema
  • use new PersonalAccessToken model for PATs
  • create PAT from frontend
  • use APIView rather than ModelViewSet for PATs
  • chore: remove dead code
  • fix PAT retrieval
  • update PAT create schema
  • fix schemas
  • only show PAT once right after it is created
  • add translations
  • add button to copy PAT
  • implement proper PAT deletion in the backend
  • implement PAT deletion in the frontend
  • add schema prop to ConfirmModal

Summary by CodeRabbit

  • New Features

    • Introduced Personal Access Token (PAT) management, allowing users to create, view, and delete PATs from their profile settings.
    • Added UI components and modals for generating and securely displaying PATs, with copy-to-clipboard functionality.
    • Implemented API endpoints for listing, creating, and deleting PATs.
    • Added support for schema-based form validation when managing PATs.
  • Bug Fixes

    • Corrected token limit configuration to restrict users to a maximum of 5 active tokens.
  • Documentation

    • Updated documentation to reflect the availability of Personal Access Tokens.
  • Style

    • Enhanced localization with new messages and warnings related to PATs.

Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

Walkthrough

This update introduces comprehensive support for Personal Access Tokens (PATs) across both backend and frontend components. On the backend, a new PersonalAccessToken model is added, with corresponding serializers, API endpoints, and view logic for token creation, listing, and deletion. Settings are updated to enforce a per-user token limit and specify the Knox token model. The frontend integrates new UI components and modals for managing PATs within the user profile settings, including form validation, token display, and deletion confirmation. Localization strings and schema validations are updated to support the new PAT features.

Changes

File(s) Change Summary
backend/ciso_assistant/settings.py, enterprise/backend/enterprise_core/settings.py Updated REST_KNOX to limit tokens per user to 5 and added KNOX_TOKEN_MODEL setting.
backend/iam/models.py, backend/iam/migrations/0012_personalaccesstoken.py Added PersonalAccessToken model with fields for name and foreign key to Knox token, plus migration.
backend/iam/serializers.py Added PersonalAccessTokenReadSerializer for serializing PAT instances.
backend/iam/views.py Introduced PersonalAccessTokenViewSet and AuthTokenDetailView for managing PATs (list, create, delete); enhanced LogoutView to delete Knox token on logout.
backend/iam/urls.py Registered new API endpoints for listing and managing PATs.
dispatcher/README.md Updated documentation to remove "(soon to be released)" from PAT section.
frontend/messages/en.json, frontend/messages/fr.json Added localization strings for PAT management (labels, help, warnings, notifications) in English and French.
frontend/src/lib/components/Modals/ConfirmModal.svelte Added schema prop for schema-based validation in confirmation modals.
frontend/src/lib/utils/schemas.ts Added AuthTokenCreateSchema for validating PAT creation forms.
frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts Added logic to load, create, and delete PATs, including form handling and validation.
frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte Added UI for listing, creating, and deleting PATs, including modal integration.
frontend/src/routes/(app)/(internal)/my-profile/settings/pat/components/CreatePATModal.svelte New component for PAT creation modal, handling form submission and token display.
frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte Passed schema prop to ConfirmModal for validation.
backend/app_tests/api/test_api_personal_access_tokens.py Added comprehensive pytest tests for PAT API endpoints covering creation, retrieval, authentication, expiry, and limits.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant KnoxTokenModel
    participant Database

    User->>Frontend: Requests to create PAT
    Frontend->>Backend: POST /iam/auth-tokens/ (name, expiry)
    Backend->>KnoxTokenModel: Create AuthToken (with expiry)
    KnoxTokenModel-->>Backend: Returns AuthToken and token string
    Backend->>Database: Create PersonalAccessToken linked to AuthToken
    Database-->>Backend: Confirms creation
    Backend->>Frontend: Returns PAT info and token string
    Frontend->>User: Displays token (once), updates PAT list

    User->>Frontend: Requests to delete PAT
    Frontend->>Backend: DELETE /iam/auth-tokens/{digest}/
    Backend->>Database: Verify and delete PersonalAccessToken and linked AuthToken
    Database-->>Backend: Confirms deletion
    Backend->>Frontend: Returns success
    Frontend->>User: Updates PAT list
Loading

Suggested reviewers

  • ab-smith

Poem

In the meadow of code, a new token appears,
For rabbits and humans to banish their fears.
With settings and schemas, the groundwork is laid,
A modal pops up—let your PAT be displayed!
But copy it quick, for it hops out of sight—
Five tokens per user, all managed just right.
🐇✨ Tokens for all, and to all, a safe night!

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@nas-tabchiche nas-tabchiche changed the title feat/pat feat: personal access tokens (PATs) Apr 28, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🔭 Outside diff range comments (1)
backend/iam/migrations/0012_personalaccesstoken.py (1)

1-25: ⚠️ Potential issue

Pipeline failing – run ruff format (and lint-driven clean-ups) before merging

CI is red because Ruff’s formatter would rewrite this file.
Running

ruff format backend/iam/migrations/0012_personalaccesstoken.py

locally (or adding it to pre-commit) will fix whitespace/blank-line issues and unblock the build.

🧰 Tools
🪛 GitHub Actions: Backend Linters

[error] 1-1: Ruff formatting check failed. File would be reformatted. Run 'ruff format' to fix code style issues.

🧹 Nitpick comments (14)
backend/iam/migrations/0012_personalaccesstoken.py (1)

19-22: Optionally add a uniqueness safeguard for token names

Right now multiple rows can share the same name for the same user because the uniqueness is only guaranteed by the auth_token FK.
If you want to avoid name clashes per user, you could add a DB/ORM-level constraint:

+        migrations.AddConstraint(
+            model_name="personalaccesstoken",
+            constraint=models.UniqueConstraint(
+                fields=["auth_token", "name"], name="unique_pat_name_per_user"
+            ),
+        ),

That would prevent accidental duplicates without extra app logic.

frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte (2)

17-18: Consistent casing for the imported component

CreatePatModal (camel-case Pat) is imported from CreatePATModal.svelte (upper-case PAT).
While Svelte is case-sensitive at the file-system level, the mixed spelling can be confusing for grep/refactoring.

-import CreatePatModal from './pat/components/CreatePATModal.svelte';
+import CreatePATModal from './pat/components/CreatePATModal.svelte';

…and update the two ref usages accordingly.


94-104: Rename deletion payload key from iddigest for clarity

You’re passing the token digest, not the DB id:

- _form: defaults({ id }, zod(z.object({ id: z.string() }))),
- schema: zod(z.object({ id: z.string() })),
+ _form: defaults({ digest: id }, zod(z.object({ digest: z.string() }))),
+ schema: zod(z.object({ digest: z.string() })),

Renaming keeps the semantic intent crystal-clear and prevents accidental overlap with other objects that do use numeric IDs.

backend/iam/views.py (1)

117-120: Use the token model helper for the quota check

request.user.auth_token_set assumes the default Knox model.
For swappable models keep it generic:

-from request.user.auth_token_set.filter(expiry__gt=now)
+from get_token_model().objects.filter(user=request.user, expiry__gt=now)

This aligns with create_token() and avoids subtle bugs when KNOX_TOKEN_MODEL is overridden.

frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts (4)

50-52: Consider moving schema definition to a shared location.

The deletion schema is defined inline in the load function and again in the deletePAT action. Consider moving this to the schemas.ts file alongside AuthTokenCreateSchema for better reusability.

// In $lib/utils/schemas.ts
+ export const AuthTokenDeleteSchema = z.object({ id: z.string() });

// Then in this file
- const personalAccessTokenDeleteForm = await superValidate(zod(z.object({ id: z.string() })));
+ const personalAccessTokenDeleteForm = await superValidate(zod(AuthTokenDeleteSchema));

173-173: Remove debug logging statement.

This appears to be a development trace that should be removed before production.

-		console.debug('deletePAT 1');

174-197: PAT deletion implementation is secure and follows existing patterns.

The implementation properly handles form validation, API requests, and user feedback. Consider using the same pattern as the createPAT action for consistency in the return statement.

For consistency with the createPAT action, consider:

-		return message(form, { status: response.status });
+		console.debug('Deleted PAT', { status: response.status });
+		setFlash({ type: 'success', message: m.successfullyDeletedPersonalAccessToken() }, event);
+		return message(form, { status: response.status });

177-177: Use shared schema for deletePAT validation.

For consistency and maintainability, consider extracting this schema definition to your schemas.ts file as suggested earlier.

- const form = await superValidate(formData, zod(z.object({ id: z.string() })));
+ const form = await superValidate(formData, zod(AuthTokenDeleteSchema));
frontend/src/routes/(app)/(internal)/my-profile/settings/pat/components/CreatePATModal.svelte (6)

1-5: Improve typing for parent prop.

The parent prop is typed as any, which could lead to potential issues. Consider creating a proper interface for the expected parent properties.

- export let parent: any;
+ interface ParentProps {
+   onConfirm: () => void;
+   buttonNeutral: string;
+   regionFooter: string;
+ }
+ export let parent: ParentProps;

49-49: Remove development console.log.

This appears to be a development debugging statement that should be removed before production.

- $: console.log('toto', $page.form?.form?.message?.data?.token);

79-81: Update test ID to match PAT functionality.

The test ID refers to TOTP (Time-based One-Time Password) instead of PAT (Personal Access Token).

- data-testid="activate-totp-confirm-button"
+ data-testid="generate-pat-button"

84-97: Token display with copy functionality is well implemented.

Good use of a warning message to inform the user that the token will only be displayed once. The copy button integration is implemented correctly.

Consider adding visual feedback when copying succeeds:

<button
  type="button"
  class="btn px-2 py-1 {parent.buttonNeutral} rounded-l-none"
- use:copy={{ text: $page?.form?.form?.message?.data?.token }}
+ use:copy={{ 
+   text: $page?.form?.form?.message?.data?.token,
+   onCopied: () => {
+     // Add toast notification or brief visual feedback
+   }
+ }}
  ><i class="fa-solid fa-copy mr-2"></i>{m.copy()}</button
>

90-90: Extract token path to a derived store for cleaner code.

The nested path to access the token is repeated and could be simplified.

+ $: token = $page?.form?.form?.message?.data?.token || '';

- <pre>{$page?.form?.form?.message?.data?.token}</pre>
+ <pre>{token}</pre>

// And update the copy directive:
- use:copy={{ text: $page?.form?.form?.message?.data?.token }}
+ use:copy={{ text: token }}

98-106: Update test ID in confirmation button.

Similar to the previous suggestion, update the test ID to match PAT functionality.

- data-testid="activate-totp-confirm-button"
+ data-testid="pat-done-button"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc59b38 and 56a33dc.

📒 Files selected for processing (14)
  • backend/ciso_assistant/settings.py (1 hunks)
  • backend/iam/migrations/0012_personalaccesstoken.py (1 hunks)
  • backend/iam/models.py (2 hunks)
  • backend/iam/serializers.py (2 hunks)
  • backend/iam/urls.py (2 hunks)
  • backend/iam/views.py (4 hunks)
  • dispatcher/README.md (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/src/lib/components/Modals/ConfirmModal.svelte (2 hunks)
  • frontend/src/lib/utils/schemas.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts (3 hunks)
  • frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte (3 hunks)
  • frontend/src/routes/(app)/(internal)/my-profile/settings/pat/components/CreatePATModal.svelte (1 hunks)
  • frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
backend/iam/serializers.py (2)
backend/iam/models.py (3)
  • PersonalAccessToken (867-888)
  • User (389-595)
  • expiry (880-881)
backend/core/serializer_fields.py (1)
  • FieldsRelatedField (24-86)
frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts (2)
frontend/src/lib/utils/constants.ts (1)
  • BASE_API_URL (3-7)
frontend/src/lib/utils/schemas.ts (1)
  • AuthTokenCreateSchema (687-690)
backend/iam/urls.py (1)
backend/iam/views.py (2)
  • AuthTokenDetailViewSet (139-155)
  • AuthTokenListViewSet (68-136)
🪛 GitHub Actions: Backend Linters
backend/iam/migrations/0012_personalaccesstoken.py

[error] 1-1: Ruff formatting check failed. File would be reformatted. Run 'ruff format' to fix code style issues.

🪛 Ruff (0.8.2)
backend/iam/serializers.py

2-2: django.utils.timezone imported but unused

Remove unused import: django.utils.timezone

(F401)

backend/iam/views.py

14-14: knox.views.user_logged_in imported but unused

Remove unused import: knox.views.user_logged_in

(F401)


15-15: rest_framework.viewsets imported but unused

Remove unused import: rest_framework.viewsets

(F401)


29-29: .serializers.PersonalAccessTokenCreateSerializer imported but unused

Remove unused import: .serializers.PersonalAccessTokenCreateSerializer

(F401)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: functional-tests (3.12, chromium)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: build (3.12)
🔇 Additional comments (22)
backend/ciso_assistant/settings.py (2)

275-275: Token limit configuration added for PATs.

Setting TOKEN_LIMIT_PER_USER to 5 enforces the requirement mentioned in the PR objectives to limit personal access tokens to a maximum of five per user.


282-282: Knox token model explicitly defined.

The new KNOX_TOKEN_MODEL setting specifies the model to be used for token authentication, which is necessary for the new Personal Access Token functionality.

dispatcher/README.md (1)

140-140: Documentation updated to reflect PAT feature availability.

The "soon to be released" phrase has been removed from the token-based authentication section heading, correctly reflecting that the Personal Access Tokens feature is now implemented.

frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte (1)

36-36: Validation schema added to ConfirmModal.

A schema property was added to the ConfirmModal component props, using the same Zod validation schema already used in _form. This enhances form validation consistency.

frontend/src/lib/components/Modals/ConfirmModal.svelte (2)

22-22: Schema prop added to ConfirmModal component.

A new exportable property schema has been added to accept validation schemas from parent components.


53-59: SuperForm updated to use the schema for validation.

The SuperForm component now includes a validators attribute that uses the provided schema, enabling proper form validation.

frontend/src/lib/utils/schemas.ts (1)

687-690: LGTM: Clean and concise schema for PAT creation.

The schema correctly validates the required fields for creating a personal access token. The name field requires a non-empty string, and the expiry field is an optional positive number (presumably days until expiration).

frontend/messages/en.json (1)

1474-1477: LGTM: Comprehensive localization strings for PAT management.

The localization strings provide clear and informative messages for all PAT operations, including helpful warnings and instructions about token security and one-time display.

Also applies to: 1479-1485

backend/iam/urls.py (2)

5-7: LGTM: Clean imports for new view classes.

The imports are correctly added for the new AuthToken view classes.


35-40: LGTM: Properly defined REST API endpoints for PAT management.

The URL patterns follow RESTful conventions with appropriate naming and structure:

  • /auth-tokens/ for listing and creating tokens
  • /auth-tokens/<str:pk>/ for operations on specific tokens

These endpoints align with the implementation in the view classes.

backend/iam/models.py (2)

39-39: LGTM: Better import practice for settings.

Using django.conf.settings instead of a direct import is a better practice for Django applications.


867-889: LGTM: Well-structured PersonalAccessToken model.

The PersonalAccessToken model is well-designed:

  • Links to the Knox token model with a ForeignKey
  • Provides convenient property accessors for token metadata
  • Includes a clear string representation
  • Aligns with the PR objective of implementing personal access tokens

The model correctly delegates to the underlying auth_token for properties like creation time, expiry, and digest, avoiding data duplication.

frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte (1)

203-210: Date parsing may break with custom backend format

new Date(pat.expiry) works only if expiry is ISO-8601 or RFC-2822.
The backend uses knox_settings.EXPIRY_DATETIME_FORMAT, which can be customised (e.g. "%d/%m/%Y %H:%M"). JS’s Date constructor would then return Invalid Date.

Two lightweight fixes:

- {m.expiresOn({ date: new Date(pat.expiry).toLocaleDateString() })}
+ {m.expiresOn({ date: dayjs(pat.expiry, 'YYYY-MM-DD HH:mm:ss').format('LL') })}

(or) ask the backend to send ISO timestamps (DateTimeField(format=None)), keeping the existing line intact.

Choose whichever side is easier to change but make them consistent.

frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts (4)

1-11: Imports look good with appropriate organization.

The new imports for PAT functionality are properly organized, with schema imports for form validation and necessary utilities from the superforms library.


43-50: PAT data fetching implementation is well structured.

The implementation follows existing patterns in the codebase for API requests with proper error handling. Good job using standard fetch patterns consistent with the rest of the file.


53-62: Return object properly extended with PAT data.

The return object has been cleanly extended to include the personal access tokens and related forms.


145-171: PAT creation implementation is thorough and follows best practices.

The createPAT action properly validates form data, handles API responses, and provides appropriate user feedback. Good implementation.

frontend/src/routes/(app)/(internal)/my-profile/settings/pat/components/CreatePATModal.svelte (5)

6-22: Imports and store setup follow best practices.

Good job organizing imports by category and using the appropriate stores and components for the functionality.


23-38: Form configuration is well implemented.

The superForm setup with appropriate validation, data types, and event handling shows good attention to detail and follows best practices.


39-48: Base styles and imports are organized well.

The component organization maintains a clear separation between imports, configuration, and styling.


52-66: Modal structure is well organized.

The modal implementation with appropriate header and layout structure follows UI patterns effectively.


67-83: Form implementation for token creation looks good.

The form fields are well structured with appropriate labels and help text. The validation integration works correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
backend/iam/views.py (2)

15-16: Remove unused imports to keep the codebase clean.

-from knox.views import DateTimeField, LoginView as KnoxLoginView, user_logged_in
-from rest_framework import permissions, serializers, status, views, viewsets
+from knox.views import DateTimeField, LoginView as KnoxLoginView
+from rest_framework import permissions, serializers, status, views
🧰 Tools
🪛 Ruff (0.8.2)

15-15: knox.views.user_logged_in imported but unused

Remove unused import: knox.views.user_logged_in

(F401)


16-16: rest_framework.viewsets imported but unused

Remove unused import: rest_framework.viewsets

(F401)


121-134: ⚠️ Potential issue

Harden input validation in token creation.

The current implementation doesn't properly validate the name and expiry inputs, which could lead to unexpected behavior or server errors.

     def post(self, request, format=None):
         token_limit_per_user = self.get_token_limit_per_user()
         name = request.data.get("name")
-        expiry = request.data.get("expiry", "30")
+        
+        # Validate name is not empty
+        if not name or not name.strip():
+            return Response(
+                {"error": "Name is required and cannot be empty."},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+        
+        # Validate expiry is a positive integer
+        try:
+            expiry_days = int(request.data.get("expiry", 30))
+            if expiry_days <= 0:
+                raise ValueError("Expiry must be positive")
+        except (TypeError, ValueError):
+            return Response(
+                {"error": "Expiry must be a positive integer (days)."},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+        
         if token_limit_per_user is not None:
             now = timezone.now()
             token = request.user.auth_token_set.filter(expiry__gt=now)
             if token.count() >= token_limit_per_user:
                 return Response(
                     {"error": "Maximum amount of tokens allowed per user exceeded."},
                     status=status.HTTP_403_FORBIDDEN,
                 )
-        instance, token = self.create_token(timedelta(days=int(expiry)))
+        instance, token = self.create_token(timedelta(days=expiry_days))
         pat = PersonalAccessToken.objects.create(auth_token=instance, name=name)
         return self.get_post_response(request, token, pat.name, pat.auth_token)
🧹 Nitpick comments (3)
backend/iam/views.py (3)

30-31: Remove unused import.

The PersonalAccessTokenCreateSerializer is imported but not used anywhere in this file.

-    PersonalAccessTokenCreateSerializer,
     PersonalAccessTokenReadSerializer,
🧰 Tools
🪛 Ruff (0.8.2)

30-30: .serializers.PersonalAccessTokenCreateSerializer imported but unused

Remove unused import: .serializers.PersonalAccessTokenCreateSerializer

(F401)


76-91: Consider implementing pagination for token listing.

For users with many tokens, the get method should implement pagination to improve performance and reduce response size. Additionally, consider adding sorting options.

 class AuthTokenListViewSet(views.APIView):
     def get_queryset(self):
-        return PersonalAccessToken.objects.filter(auth_token__user=self.request.user)
+        queryset = PersonalAccessToken.objects.filter(auth_token__user=self.request.user)
+        # Optional: Add ordering
+        return queryset.order_by('-auth_token__created')

You might also consider using Django REST framework's pagination classes for the GET endpoint if you expect users to have many tokens in the future.


124-131: Consider caching token count to improve performance.

The current implementation queries the database to count active tokens on every token creation request. For frequently used APIs, consider caching this value.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56a33dc and 1f4e11f.

📒 Files selected for processing (1)
  • backend/iam/views.py (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/iam/views.py (2)
backend/iam/models.py (4)
  • PersonalAccessToken (867-888)
  • digest (884-885)
  • delete (440-442)
  • expiry (880-881)
backend/iam/serializers.py (3)
  • PersonalAccessTokenCreateSerializer (134-154)
  • PersonalAccessTokenReadSerializer (122-131)
  • create (139-150)
🪛 Ruff (0.8.2)
backend/iam/views.py

15-15: knox.views.user_logged_in imported but unused

Remove unused import: knox.views.user_logged_in

(F401)


16-16: rest_framework.viewsets imported but unused

Remove unused import: rest_framework.viewsets

(F401)


30-30: .serializers.PersonalAccessTokenCreateSerializer imported but unused

Remove unused import: .serializers.PersonalAccessTokenCreateSerializer

(F401)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: functional-tests (3.12, chromium)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: build (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
backend/iam/views.py (1)

136-144: LGTM! Good implementation of the GET endpoint.

The implementation correctly filters tokens by the authenticated user and uses the appropriate serializer.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
backend/iam/views.py (3)

62-68: Improve token deletion error handling in LogoutView.

The current implementation has several issues:

  1. It doesn't validate the presence of the HTTP_AUTHORIZATION header
  2. It assumes the Authorization header format is "Bearer {token}"
  3. It catches all exceptions with a broad try-except block
    try:
        logger.info("logout request", user=request.user)
        try:
-            access_token = request.META.get("HTTP_AUTHORIZATION").split(" ")[1]
-            digest = crypto.hash_token(access_token)
-            auth_token = AuthToken.objects.get(digest=digest)
-            auth_token.delete()
+            auth_header = request.META.get("HTTP_AUTHORIZATION")
+            if auth_header and " " in auth_header:
+                access_token = auth_header.split(" ")[1]
+                digest = crypto.hash_token(access_token)
+                auth_token = AuthToken.objects.get(digest=digest)
+                auth_token.delete()
+            else:
+                logger.warning("No valid authorization header found during logout", user=request.user)
        except Exception as e:
-            logger.error("Error deleting token", user=request.user, error=e)
+            logger.error("Error deleting token during logout", user=request.user, error=str(e))

142-157: Improve error handling in token deletion.

The current implementation uses a broad try-except block that catches all exceptions, and the error message might not be accurate for all cases.

    def delete(self, request, *args, **kwargs):
        try:
            token = AuthToken.objects.get(digest=kwargs["pk"])
            if token.user != request.user:
                return Response(
                    {"error": "You do not have permission to delete this token."},
                    status=status.HTTP_403_FORBIDDEN,
                )
            token.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
-        except Exception as e:
-            logger.error("Error deleting token", error=e)
+        except AuthToken.DoesNotExist:
+            logger.info("Attempt to delete non-existent token", digest=kwargs["pk"], user=request.user.id)
             return Response(
                 {"error": "Token not found or already deleted."},
                 status=status.HTTP_404_NOT_FOUND,
             )
+        except Exception as e:
+            logger.error("Error deleting token", error=str(e), digest=kwargs["pk"], user=request.user.id)
+            return Response(
+                {"error": "Failed to delete token due to an internal error."},
+                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            )

114-128: 🛠️ Refactor suggestion

Improve input validation in AuthTokenListViewSet.post.

The method lacks validation for required fields and proper error handling for invalid input types.

-    expiry = request.data.get("expiry", "30")
+    name = request.data.get("name")
+    if not name or not name.strip():
+        return Response(
+            {"error": "Name is required for the token."},
+            status=status.HTTP_400_BAD_REQUEST,
+        )
+    
+    try:
+        expiry_days = int(request.data.get("expiry", 30))
+        if expiry_days <= 0:
+            raise ValueError
+    except (TypeError, ValueError):
+        return Response(
+            {"error": "Expiry must be a positive integer (days)."},
+            status=status.HTTP_400_BAD_REQUEST,
+        )

And then update line 126 to use expiry_days instead of parsing the expiry string again:

-    instance, token = self.create_token(timedelta(days=int(expiry)))
+    instance, token = self.create_token(timedelta(days=expiry_days))
🧹 Nitpick comments (3)
backend/iam/views.py (3)

14-17: Strip unused imports to keep imports tidy.

-from knox.views import DateTimeField, LoginView as KnoxLoginView, user_logged_in
+from knox.views import DateTimeField, LoginView as KnoxLoginView

76-139: Consider using ModelViewSet for cleaner implementation.

While using APIView works, Django REST Framework's ModelViewSet could simplify the implementation for standard CRUD operations on tokens. For more customization control, consider using GenericAPIView with appropriate mixins.

This would make the code more concise and follow Django REST Framework's patterns more closely.


76-157: Add docstrings to new classes and methods for better documentation.

The new classes and methods lack comprehensive docstrings that explain their purpose, parameters, return values, and possible exceptions. This would improve the code's maintainability and help other developers understand the implementation.

+"""
+API view for listing and creating personal access tokens.
+"""

And similar docstrings for each public method.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f4e11f and 759fe40.

📒 Files selected for processing (7)
  • backend/iam/migrations/0012_personalaccesstoken.py (1 hunks)
  • backend/iam/models.py (3 hunks)
  • backend/iam/serializers.py (2 hunks)
  • backend/iam/views.py (3 hunks)
  • enterprise/backend/enterprise_core/settings.py (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • frontend/messages/en.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/iam/serializers.py
  • backend/iam/migrations/0012_personalaccesstoken.py
  • backend/iam/models.py
  • frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/iam/views.py (2)
backend/iam/models.py (6)
  • PersonalAccessToken (868-889)
  • Role (599-612)
  • RoleAssignment (615-865)
  • digest (885-886)
  • delete (441-443)
  • expiry (881-882)
backend/iam/serializers.py (1)
  • PersonalAccessTokenReadSerializer (120-129)
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: Analyze (python)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: functional-tests (3.12, chromium)
  • GitHub Check: build (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
🔇 Additional comments (5)
enterprise/backend/enterprise_core/settings.py (2)

273-273: Appropriate token limit configuration.

Setting a limit of 5 tokens per user is a good security practice that balances usability with risk management. This helps prevent token proliferation and reduces the attack surface.


280-280: Good explicit configuration of Knox token model.

Explicitly configuring KNOX_TOKEN_MODEL ensures that the application uses the right token model across all components. This is essential for the new Personal Access Token implementation.

backend/iam/views.py (3)

76-95: Good implementation of utility methods for token management.

The utility methods are well-structured and follow appropriate separation of concerns. Using Knox settings through getter methods provides flexibility and maintainability.


96-112: Clean token creation and response formatting.

The methods for token creation and response formatting are well-implemented. Breaking these into separate methods improves maintainability and testability.


129-138: Good implementation of token listing endpoint.

The GET method correctly filters tokens by the authenticated user and uses an appropriate serializer with context.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (2)
backend/app_tests/api/test_api_personal_access_tokens.py (2)

1-264: 🛠️ Refactor suggestion

Missing DELETE operation test

The PR mentions supporting full CRUD operations for PATs, but there's no test for the delete functionality.

Add a test for token deletion to ensure complete coverage of the API functionality:

@pytest.mark.django_db
def test_delete_token(self, authenticated_client, personal_access_token):
    """Test deleting a personal access token."""
    url = f"/api/iam/auth-tokens/{personal_access_token.id}/"

    response = authenticated_client.delete(url)

    assert response.status_code == status.HTTP_204_NO_CONTENT
    assert not PersonalAccessToken.objects.filter(id=personal_access_token.id).exists()

1-264: 💡 Verification agent

🧩 Analysis chain

Consider adding test for handling duplicate token names

There's no test to verify how the API handles tokens with duplicate names for the same user.


🏁 Script executed:

#!/bin/bash
# Check if there are any validation constraints for token names in the model or schema

# Search for token name validation in models.py
echo "Searching for token name validation in models.py:"
grep -r "class PersonalAccessToken" --include="*.py" .
rg "name\s*=" -A 5 -B 5 --include="*.py" | grep -i "unique"

# Check for validation in serializers
echo "Checking serializers for token name validation:"
rg "TokenSerializer|AccessTokenSerializer" -A 20 --include="*.py"

Length of output: 1518


🏁 Script executed:

#!/bin/bash
echo "Inspecting PersonalAccessToken model constraints:"
rg -n "class PersonalAccessToken" -A 30 backend/iam/models.py

echo "Searching for unique constraints across the repo:"
rg -n "unique=True" -g "*.py"

echo "Inspecting PersonalAccessToken serializer:"
rg -n "class PersonalAccessToken" -A 30 backend/iam/serializers.py

Length of output: 4367


Add duplicate token name handling and tests

The name field on PersonalAccessToken has no uniqueness constraint, so the API currently allows duplicate names for the same user. Decide on the desired behavior and update accordingly:

  • If duplicate names should be disallowed:
    • Add a unique_together or unique constraint on (user, name) in backend/iam/models.py.
    • Enforce validation in the serializer or view (e.g., raise 400 with “Token name must be unique per user”).
    • Add a test that creating a second token with the same name returns 400 and does not increase the token count.

  • If duplicate names are acceptable:
    • Add a test in test_api_personal_access_tokens.py that creating two tokens with the same name for one user succeeds (status 200) and both appear in the list endpoint.

Files to update:

  • backend/iam/models.py (define uniqueness if needed)
  • backend/iam/serializers.py or views (add validation)
  • backend/app_tests/api/test_api_personal_access_tokens.py (add the new test)
🧹 Nitpick comments (2)
backend/app_tests/api/test_api_personal_access_tokens.py (2)

227-264: Token expiration validation

The test correctly verifies that expired tokens cannot be used for authentication. However, line 259 modifies the TokenAuthentication class directly which could affect other tests.

Consider using a more isolated approach for overriding the validation behavior. For example:

- # Clear the authentication cache if needed
- TokenAuthentication.validate_user = lambda self, user: user
+ # Create a temporary TokenAuthentication instance for this test
+ auth = TokenAuthentication()
+ original_validate = auth.validate_user
+ try:
+     auth.validate_user = lambda user: user
+     # Try to use the expired token
+     expired_response = test_client.get(protected_url)
+     assert expired_response.status_code == status.HTTP_401_UNAUTHORIZED
+ finally:
+     # Restore original method
+     auth.validate_user = original_validate

106-108: Using hardcoded token limit may lead to inconsistent tests

The test uses a hardcoded token limit of 2, but the PR summary mentions a limit of 5 tokens per user.

Consider importing the actual token limit from settings to ensure tests stay in sync with application configuration:

- # Set token limit for test
- monkeypatch.setattr(knox_settings, "TOKEN_LIMIT_PER_USER", 2)
+ # Import and use the actual token limit from settings
+ from ciso_assistant.settings import TOKEN_LIMIT_PER_USER
+ # Use a smaller limit just for this test
+ monkeypatch.setattr(knox_settings, "TOKEN_LIMIT_PER_USER", min(2, TOKEN_LIMIT_PER_USER))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d67f0c1 and 4543cbf.

📒 Files selected for processing (3)
  • backend/app_tests/api/test_api_personal_access_tokens.py (1 hunks)
  • backend/iam/urls.py (2 hunks)
  • backend/iam/views.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/iam/urls.py
  • backend/iam/views.py
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: test (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: functional-tests (3.12, chromium)
  • GitHub Check: build (3.12)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (8)
backend/app_tests/api/test_api_personal_access_tokens.py (8)

16-50: Well-structured fixtures for PAT testing

The test fixtures are comprehensive and well-organized, providing all the necessary components for testing the Personal Access Token functionality. Good use of pytest fixtures to create reusable test components.


52-85: Great token creation test coverage

The tests thoroughly verify token creation with both default and custom expiry periods. The validation in lines 83-84 correctly allows for small time differences during test execution, which prevents flaky tests.


86-103: Comprehensive validation error testing

Good job testing both negative and non-numeric expiry values to ensure proper error handling in the API.


104-126: Effective token limit enforcement test

The test effectively verifies that users cannot exceed their token limit using monkeypatching to isolate the test environment.


127-147: Token retrieval tests are thorough

Both cases of retrieving existing tokens and handling empty results are correctly tested.


149-185: Good user isolation testing

The test properly verifies that users can only see their own tokens, which is a critical security requirement for PATs.


186-198: Authentication security verification

The test confirms that unauthenticated users cannot access or create tokens, ensuring proper access control.


199-226: Token authentication verification

The test successfully verifies that a generated PAT can be used to authenticate API requests, which is the primary purpose of PATs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant