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

Merged
merged 54 commits into from
May 23, 2025
Merged

feat: personal access tokens (PATs) #1924

merged 54 commits into from
May 23, 2025

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 Tokens (PATs) for API authentication, allowing users to create, view, and delete their own tokens from the profile settings page.
    • Added UI components and modals for managing PATs, including secure display and copy-to-clipboard functionality for newly created tokens.
    • Implemented localization support for PAT-related features in English and French.
  • Bug Fixes

    • Improved toast notification stacking to ensure visibility above other UI elements.
  • Documentation

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

    • Added comprehensive test coverage for Personal Access Token API endpoints, including creation, retrieval, authentication, expiry, and error handling.

Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

Walkthrough

This update introduces comprehensive support for Personal Access Tokens (PATs) across backend and frontend components. It adds new models, serializers, API endpoints, and test coverage for PAT management, along with corresponding UI elements, localization strings, and form validation schemas. Supporting settings, migrations, and documentation are also updated.

Changes

File(s) / Path(s) Change Summary
backend/ciso_assistant/settings.py
enterprise/backend/enterprise_core/settings.py
Added global setting variable KNOX_TOKEN_MODEL with value "knox.AuthToken".
backend/iam/models.py
backend/iam/migrations/0013_personalaccesstoken.py
Introduced new model PersonalAccessToken with fields for name and a foreign key to the Knox AuthToken; added corresponding migration.
backend/iam/serializers.py Added PersonalAccessTokenReadSerializer for serializing PATs.
backend/iam/views.py Enhanced LogoutView to delete Knox tokens on logout. Added PersonalAccessTokenViewSet and AuthTokenDetailView for PAT management (list, create, delete).
backend/iam/urls.py Registered new API endpoints for listing, creating, and deleting PATs.
backend/app_tests/api/test_api_personal_access_tokens.py Added comprehensive tests for PAT API endpoints, including creation, retrieval, authentication, expiry, and error handling.
backend/core/views.py Changed path construction for dummy domain fixture file in import_dummy_domain method.
dispatcher/README.md Updated documentation to reflect released status of token-based authentication.
frontend/messages/en.json
frontend/messages/fr.json
Added/updated localization strings for PAT management, including labels, descriptions, warnings, error messages, and confirmation prompts.
frontend/src/lib/components/Modals/ConfirmModal.svelte
frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte
Added schema prop to ConfirmModal for schema-based validation.
frontend/src/lib/components/Toast/Toast.svelte Changed default z-index for toast notifications from 'z-[888]' to 'z-[9999]'.
frontend/src/lib/utils/schemas.ts Added AuthTokenCreateSchema for validating PAT creation forms.
frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts Extended backend logic to support PAT listing, creation, and deletion; added corresponding form validation and actions.
frontend/src/routes/(app)/(internal)/my-profile/settings/+page.svelte Added UI section for managing PATs, including creation and deletion modals, and listing existing tokens.
frontend/src/routes/(app)/(internal)/my-profile/settings/pat/components/CreatePATModal.svelte New component for creating PATs in a modal with secure display and copy-to-clipboard functionality.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Knox
    participant DB

    User->>Frontend: Request to create PAT
    Frontend->>Backend: POST /iam/auth-tokens/ (name, expiry)
    Backend->>Knox: Create AuthToken (with expiry)
    Knox-->>Backend: AuthToken instance
    Backend->>DB: Create PersonalAccessToken (links AuthToken)
    DB-->>Backend: PAT record
    Backend-->>Frontend: Return token string and metadata
    Frontend-->>User: Display token (once)
Loading
sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant DB

    User->>Frontend: Request to delete PAT
    Frontend->>Backend: DELETE /iam/auth-tokens/{digest}/
    Backend->>DB: Verify PAT ownership and existence
    DB-->>Backend: PAT record (if exists)
    Backend->>DB: Delete PAT and linked AuthToken
    DB-->>Backend: Success
    Backend-->>Frontend: Success response
    Frontend-->>User: Show confirmation
Loading

Suggested reviewers

  • ab-smith

Poem

🐇
Tokens for bunnies, secure and neat,
Now in your profile, a personal treat!
Create, copy, delete with flair—
The UI and backend both handle with care.
One hop for security, two hops for fun,
PATs for the CISO—now everyone’s won!
🥕


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 672ba44 and 20703e1.

📒 Files selected for processing (1)
  • backend/core/views.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/core/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: functional-tests (3.12, chromium)
  • GitHub Check: build (3.12)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
✨ 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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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: 1

🧹 Nitpick comments (3)
backend/app_tests/api/test_api_personal_access_tokens.py (3)

9-9: Remove the unused TokenAuthentication import

TokenAuthentication is never used in this module and Ruff (F401) is already flagging it. Eliminating dead imports keeps the test file tidy and prevents --fail-on-error pipelines from breaking.

-from knox.auth import TokenAuthentication
🧰 Tools
🪛 Ruff (0.8.2)

9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)


56-63: Hard-coding endpoint paths makes tests brittle

Every POST/GET in this suite uses the literal "/api/iam/auth-tokens/".
If the router prefix changes (e.g., versioning /v2/), all these tests break simultaneously.

DRF’s reverse() (or django.urls.reverse) keeps the tests resilient to URL refactors:

-from_path = "/api/iam/auth-tokens/"
+from django.urls import reverse
+
+from_path = reverse("iam:personal-access-token-list")

Consider introducing a fixture that returns this value so you can reuse it across all tests.
Same applies to the "/api/risk-assessments/" URL in the authentication tests.


234-244: expiry expressed in “days” is implicit – document or validate

"expiry": 1 is assumed to mean “1 day” in the API, yet nothing in the payload conveys units.
For clarity and future-proofing:

  • Add an explicit comment or docstring in the test explaining that the backend interprets this as days, or
  • Change the payload to use an ISO-8601 duration string or seconds field if the API supports it.

This prevents misinterpretation if the parameter semantics evolve.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 202e5f3 and a8ed2ac.

📒 Files selected for processing (1)
  • backend/app_tests/api/test_api_personal_access_tokens.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
backend/app_tests/api/test_api_personal_access_tokens.py

9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)

⏰ 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: functional-tests (3.12, chromium)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: build (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
backend/app_tests/api/test_api_personal_access_tokens.py (1)

61-63: Creation should return HTTP 201, not 200

A successful resource creation conventionally answers with 201 Created.
If the view is deliberately returning 200, confirm this is intentional; otherwise, update either the view or the assertion:

-assert response.status_code == status.HTTP_200_OK
+assert response.status_code == status.HTTP_201_CREATED

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

🧹 Nitpick comments (2)
backend/app_tests/api/test_api_personal_access_tokens.py (2)

9-9: Remove unused import.

The TokenAuthentication class is imported but not used anywhere in this test file.

-from knox.auth import TokenAuthentication
🧰 Tools
🪛 Ruff (0.8.2)

9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)


100-103: Add explicit error message assertion for non-integer expiry test.

The test for negative expiry (above) explicitly checks for a specific error message, but this test for non-integer expiry doesn't. For consistency and completeness, add an assertion for the error message content.

data["expiry"] = "not-a-number"
response = authenticated_client.post(url, data)
assert response.status_code == status.HTTP_400_BAD_REQUEST
+assert "error" in response.data
+assert "Expiry must be a positive integer" in response.data["error"]
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b5814a0 and a480db4.

📒 Files selected for processing (1)
  • backend/app_tests/api/test_api_personal_access_tokens.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
backend/app_tests/api/test_api_personal_access_tokens.py

9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)

⏰ 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: functional-tests (3.12, chromium)
  • GitHub Check: test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: build (3.12)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
backend/app_tests/api/test_api_personal_access_tokens.py (3)

125-125: LGTM! User-scoped token count check is correct.

This line correctly filters tokens by user, which prevents test failures if other fixtures or tests create tokens for different users.


207-225: LGTM! Comprehensive token authentication test.

This test properly verifies that a personal access token can be used for authentication by:

  1. Creating a token
  2. Confirming access is denied without authentication
  3. Using the token to authenticate
  4. Verifying successful access to protected resources

The error messages in assertions also provide helpful debugging context if the test fails.


226-269: LGTM! Well-designed token expiration test.

This test thoroughly validates the token expiration functionality by:

  1. Creating a token and verifying it works initially
  2. Manually setting the expiry time in the past
  3. Confirming the expired token is properly rejected

The assertion error messages include detailed information for easier debugging.

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: 3

🧹 Nitpick comments (2)
backend/iam/views.py (2)

97-99: Consider making token limit configurable.

The token limit is currently hardcoded to 5. For better flexibility, consider making this configurable through settings, similar to how Knox settings are used elsewhere in the code.

-    def get_token_limit_per_user(self):
-        return 5
+    def get_token_limit_per_user(self):
+        return getattr(settings, 'KNOX_TOKEN_LIMIT_PER_USER', 5)

This would allow administrators to adjust the limit without code changes.


143-146: Consider internationalizing the error message limit.

The error message "errorMaxPatAmountExceeded" appears to be a translation key, but it's not wrapped in the gettext function for translation, unlike other messages in the codebase.

-                    {"error": "errorMaxPatAmountExceeded"},
+                    {"error": _("errorMaxPatAmountExceeded")},
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c72a005 and 1cad2db.

📒 Files selected for processing (9)
  • backend/ciso_assistant/settings.py (1 hunks)
  • backend/iam/views.py (3 hunks)
  • enterprise/backend/enterprise_core/settings.py (1 hunks)
  • frontend/messages/en.json (2 hunks)
  • frontend/messages/fr.json (1 hunks)
  • frontend/src/lib/components/Toast/Toast.svelte (1 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/pat/components/CreatePATModal.svelte (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • frontend/src/lib/components/Toast/Toast.svelte
  • enterprise/backend/enterprise_core/settings.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • backend/ciso_assistant/settings.py
  • frontend/messages/fr.json
  • frontend/src/lib/utils/schemas.ts
  • frontend/src/routes/(app)/(internal)/my-profile/settings/pat/components/CreatePATModal.svelte
  • frontend/messages/en.json
  • frontend/src/routes/(app)/(internal)/my-profile/settings/+page.server.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/iam/views.py (3)
backend/iam/models.py (7)
  • Folder (58-229)
  • PersonalAccessToken (907-928)
  • Role (638-651)
  • RoleAssignment (654-904)
  • digest (924-925)
  • delete (455-457)
  • expiry (920-921)
backend/iam/serializers.py (1)
  • PersonalAccessTokenReadSerializer (120-129)
backend/app_tests/api/test_api_personal_access_tokens.py (2)
  • auth_token (33-35)
  • user (17-18)
⏰ 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: functional-tests (3.12, chromium)
  • GitHub Check: test (3.12)
  • 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)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
backend/iam/views.py (1)

62-79: Great implementation of token deletion in LogoutView.

The implementation follows best practices by:

  1. Checking for valid authorization header format
  2. Properly handling token extraction and deletion
  3. Adding detailed logging for debugging

This addresses the previous suggestion to improve error handling in the logout process.

Comment on lines +125 to +149
def post(self, request, format=None):
token_limit_per_user = self.get_token_limit_per_user()
name = request.data.get("name")
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,
)
if token_limit_per_user is not None:
now = timezone.now()
token = request.user.auth_token_set.filter(expiry__gt=now).filter(
personalaccesstoken__isnull=False
)
if token.count() >= token_limit_per_user:
return Response(
{"error": "errorMaxPatAmountExceeded"},
status=status.HTTP_403_FORBIDDEN,
)
instance, token = self.create_token(timedelta(days=int(expiry_days)))
pat = PersonalAccessToken.objects.create(auth_token=instance, name=name)
return self.get_post_response(request, token, pat.name, pat.auth_token)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Input validation could be strengthened for token creation.

The post method validates expiry_days thoroughly but doesn't validate that name is provided and non-empty. This could lead to anonymous entries in the token list.

Also, consider adding validation for maximum name length to prevent very long token names.

🤖 Prompt for AI Agents
In backend/iam/views.py around lines 125 to 149, the post method lacks
validation for the 'name' field, allowing empty or missing names which can cause
anonymous token entries. Add a check to ensure 'name' is provided and not empty,
returning a 400 error if invalid. Also, enforce a maximum length constraint on
'name' to prevent excessively long token names, returning an appropriate error
response if exceeded.

Comment on lines +162 to +193
class AuthTokenDetailView(views.APIView):
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 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,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Excellent error handling in AuthTokenDetailView.

The implementation follows best practices by:

  1. Using specific exception handlers for different error cases
  2. Providing clear error messages
  3. Including detailed logging with contextual information
  4. Returning appropriate HTTP status codes

However, there's a potential issue: the view doesn't verify if the token is a personal access token. It operates directly on the AuthToken model, which could allow deletion of session tokens.


🏁 Script executed:

#!/bin/bash
# Check if the view filters tokens to only delete PersonalAccessTokens

# Look for PersonalAccessToken references in the deletion logic
rg -A 5 "AuthToken.objects.get\(digest=kwargs\[\"pk\"\]\)" backend/iam/views.py

# Check if there's filtering on personalaccesstoken relation
rg "personalaccesstoken" backend/iam/views.py

Length of output: 469


Ensure deletion is scoped to personal access tokens only

The current delete method in backend/iam/views.py (lines 162–193) fetches any AuthToken by digest and allows the owner to delete it—even session tokens. To prevent accidental removal of non-personal tokens, scope the lookup to only tokens with an associated PersonalAccessToken.

• File: backend/iam/views.py, AuthTokenDetailView.delete
• Replace:

token = AuthToken.objects.get(digest=kwargs["pk"])

with something like:

token = AuthToken.objects.filter(
    personalaccesstoken__isnull=False,
    digest=kwargs["pk"]
).first()
if not token:
    return Response(
        {"error": "Personal access token not found."},
        status=status.HTTP_404_NOT_FOUND,
    )

This ensures only personal access tokens can be deleted.

🤖 Prompt for AI Agents
In backend/iam/views.py within AuthTokenDetailView.delete method (lines
162-193), the code fetches any AuthToken by digest, allowing deletion of session
tokens. Modify the token lookup to filter only tokens linked to a
PersonalAccessToken by using a filter with personalaccesstoken__isnull=False and
digest=kwargs["pk"], then check if the token exists before proceeding. If no
token is found, return a 404 response indicating the personal access token was
not found. This change restricts deletion to personal access tokens only.

Comment on lines +87 to +160
class PersonalAccessTokenViewSet(views.APIView):
def get_queryset(self):
return PersonalAccessToken.objects.filter(auth_token__user=self.request.user)

def get_context(self):
return {"request": self.request, "format": self.format_kwarg, "view": self}

def get_token_prefix(self):
return knox_settings.TOKEN_PREFIX

def get_token_limit_per_user(self):
return 5

def get_expiry_datetime_format(self):
return knox_settings.EXPIRY_DATETIME_FORMAT

def format_expiry_datetime(self, expiry):
datetime_format = self.get_expiry_datetime_format()
return DateTimeField(format=datetime_format).to_representation(expiry)

def create_token(self, expiry):
token_prefix = self.get_token_prefix()
return get_token_model().objects.create(
user=self.request.user, expiry=expiry, prefix=token_prefix
)

def get_post_response_data(self, request, token, name, instance):
data = {
"name": name,
"expiry": self.format_expiry_datetime(instance.expiry),
"token": token,
}
return data

def get_post_response(self, request, token, name, instance):
data = self.get_post_response_data(request, token, name, instance)
return Response(data)

def post(self, request, format=None):
token_limit_per_user = self.get_token_limit_per_user()
name = request.data.get("name")
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,
)
if token_limit_per_user is not None:
now = timezone.now()
token = request.user.auth_token_set.filter(expiry__gt=now).filter(
personalaccesstoken__isnull=False
)
if token.count() >= token_limit_per_user:
return Response(
{"error": "errorMaxPatAmountExceeded"},
status=status.HTTP_403_FORBIDDEN,
)
instance, token = self.create_token(timedelta(days=int(expiry_days)))
pat = PersonalAccessToken.objects.create(auth_token=instance, name=name)
return self.get_post_response(request, token, pat.name, pat.auth_token)

def get(self, request, *args, **kwargs):
"""
Get all personal access tokens for the user.
"""
queryset = self.get_queryset()
serializer = PersonalAccessTokenReadSerializer(
queryset, many=True, context=self.get_context()
)
return Response(serializer.data)

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Well-structured implementation of PersonalAccessTokenViewSet.

The implementation provides a robust API for creating and listing personal access tokens with proper validation for expiry and enforcing token limits.

However, there's a potential issue with the name parameter validation:

-        name = request.data.get("name")
+        name = request.data.get("name")
+        if not name:
+            return Response(
+                {"error": "Name is required for personal access tokens."},
+                status=status.HTTP_400_BAD_REQUEST,
+            )

This would prevent anonymous token creation and align with input validation practices applied to the expiry parameter.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class PersonalAccessTokenViewSet(views.APIView):
def get_queryset(self):
return PersonalAccessToken.objects.filter(auth_token__user=self.request.user)
def get_context(self):
return {"request": self.request, "format": self.format_kwarg, "view": self}
def get_token_prefix(self):
return knox_settings.TOKEN_PREFIX
def get_token_limit_per_user(self):
return 5
def get_expiry_datetime_format(self):
return knox_settings.EXPIRY_DATETIME_FORMAT
def format_expiry_datetime(self, expiry):
datetime_format = self.get_expiry_datetime_format()
return DateTimeField(format=datetime_format).to_representation(expiry)
def create_token(self, expiry):
token_prefix = self.get_token_prefix()
return get_token_model().objects.create(
user=self.request.user, expiry=expiry, prefix=token_prefix
)
def get_post_response_data(self, request, token, name, instance):
data = {
"name": name,
"expiry": self.format_expiry_datetime(instance.expiry),
"token": token,
}
return data
def get_post_response(self, request, token, name, instance):
data = self.get_post_response_data(request, token, name, instance)
return Response(data)
def post(self, request, format=None):
token_limit_per_user = self.get_token_limit_per_user()
name = request.data.get("name")
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,
)
if token_limit_per_user is not None:
now = timezone.now()
token = request.user.auth_token_set.filter(expiry__gt=now).filter(
personalaccesstoken__isnull=False
)
if token.count() >= token_limit_per_user:
return Response(
{"error": "errorMaxPatAmountExceeded"},
status=status.HTTP_403_FORBIDDEN,
)
instance, token = self.create_token(timedelta(days=int(expiry_days)))
pat = PersonalAccessToken.objects.create(auth_token=instance, name=name)
return self.get_post_response(request, token, pat.name, pat.auth_token)
def get(self, request, *args, **kwargs):
"""
Get all personal access tokens for the user.
"""
queryset = self.get_queryset()
serializer = PersonalAccessTokenReadSerializer(
queryset, many=True, context=self.get_context()
)
return Response(serializer.data)
def post(self, request, format=None):
token_limit_per_user = self.get_token_limit_per_user()
- name = request.data.get("name")
+ name = request.data.get("name")
+ if not name:
+ return Response(
+ {"error": "Name is required for personal access tokens."},
+ 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,
)
if token_limit_per_user is not None:
now = timezone.now()
token = request.user.auth_token_set.filter(expiry__gt=now).filter(
personalaccesstoken__isnull=False
)
if token.count() >= token_limit_per_user:
return Response(
{"error": "errorMaxPatAmountExceeded"},
status=status.HTTP_403_FORBIDDEN,
)
instance, token = self.create_token(timedelta(days=int(expiry_days)))
pat = PersonalAccessToken.objects.create(auth_token=instance, name=name)
return self.get_post_response(request, token, pat.name, pat.auth_token)
🤖 Prompt for AI Agents
In backend/iam/views.py around lines 87 to 160, the 'name' parameter from the
request data is used without validation, which could lead to issues if it is
missing or invalid. Add validation to ensure 'name' is provided and is a
non-empty string before proceeding with token creation. If 'name' is invalid,
return a 400 Bad Request response with an appropriate error message, similar to
the expiry parameter validation.

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

🧹 Nitpick comments (2)
backend/app_tests/api/test_api_personal_access_tokens.py (2)

8-9: Remove unused imports

These imports are not being used in the test file:

-from knox.settings import knox_settings
-from knox.auth import TokenAuthentication
🧰 Tools
🪛 Ruff (0.11.9)

8-8: knox.settings.knox_settings imported but unused

Remove unused import: knox.settings.knox_settings

(F401)


9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)


233-233: Consider parameterizing expiry tests

You've written good tests for token expiry, but consider using pytest's parameterize feature to test multiple expiry values in a single test to make the test suite more maintainable.

@pytest.mark.parametrize("expiry_days,expected_status", [
    (1, status.HTTP_200_OK),
    (30, status.HTTP_200_OK),
    (365, status.HTTP_200_OK),
    (-5, status.HTTP_400_BAD_REQUEST),
    ("not-a-number", status.HTTP_400_BAD_REQUEST),
])
def test_create_token_with_various_expiry_values(self, authenticated_client, expiry_days, expected_status):
    url = "/api/iam/auth-tokens/"
    data = {"name": f"Token with expiry {expiry_days}", "expiry": expiry_days}
    
    response = authenticated_client.post(url, data)
    assert response.status_code == expected_status
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1cad2db and 2a5b1b0.

📒 Files selected for processing (1)
  • backend/app_tests/api/test_api_personal_access_tokens.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
backend/app_tests/api/test_api_personal_access_tokens.py

8-8: knox.settings.knox_settings imported but unused

Remove unused import: knox.settings.knox_settings

(F401)


9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)

⏰ 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: build (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: functional-tests (3.12, chromium)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
backend/app_tests/api/test_api_personal_access_tokens.py (2)

52-264: Test suite looks comprehensive - nice work!

The test coverage for Personal Access Tokens is thorough, covering token creation, retrieval, authentication, expiration, and authorization. You've effectively tested all the key user scenarios and edge cases.


120-120: Good fix for user-scoped token assertion

I see you've properly implemented the token-limit assertion to be scoped to the specific user. This is the correct approach to avoid test failures when other users have tokens in the database.

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 (1)
backend/app_tests/api/test_api_personal_access_tokens.py (1)

52-265: 🛠️ Refactor suggestion

Missing test for token deletion functionality.

The test suite covers creation, retrieval, authentication, and expiry scenarios for Personal Access Tokens, but it's missing tests for the token deletion functionality mentioned in the PR objectives.

Consider adding a test case to verify that:

  1. Users can delete their own tokens
  2. The deleted tokens can no longer be used for authentication
  3. Users cannot delete tokens belonging to other users

Example implementation:

@pytest.mark.django_db
def test_delete_token(self, authenticated_client, user):
    """Test deleting a personal access token."""
    # Create a token first
    url = "/api/iam/auth-tokens/"
    response = authenticated_client.post(url, {"name": "Delete Test Token", "expiry": 30})
    assert response.status_code == status.HTTP_200_OK
    token_value = response.data["token"]
    
    # Get the token ID
    get_response = authenticated_client.get(url)
    assert get_response.status_code == status.HTTP_200_OK
    token_id = get_response.data[0]["id"]
    
    # Delete the token
    delete_url = f"{url}{token_id}/"
    delete_response = authenticated_client.delete(delete_url)
    assert delete_response.status_code == status.HTTP_204_NO_CONTENT
    
    # Verify token is deleted
    get_response_after = authenticated_client.get(url)
    assert len(get_response_after.data) == 0
    
    # Verify token can't be used for authentication
    test_client = APIClient()
    test_client.credentials(HTTP_AUTHORIZATION=f"Token {token_value}")
    protected_url = "/api/risk-assessments/"
    auth_response = test_client.get(protected_url)
    assert auth_response.status_code == status.HTTP_401_UNAUTHORIZED
🧹 Nitpick comments (2)
backend/app_tests/api/test_api_personal_access_tokens.py (2)

8-9: Remove unused imports to improve code cleanliness.

The following imports are not used anywhere in the test file:

  • knox_settings from knox.settings
  • TokenAuthentication from knox.auth
-from knox.settings import knox_settings
-from knox.auth import TokenAuthentication
🧰 Tools
🪛 Ruff (0.11.9)

8-8: knox.settings.knox_settings imported but unused

Remove unused import: knox.settings.knox_settings

(F401)


9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)


100-102: Incomplete assertion for non-integer expiry validation.

The test checks that non-integer expiry values are rejected with a 400 status code, but doesn't verify the specific error message like it does for negative values.

Add an assertion to verify the error message for non-integer expiry:

data["expiry"] = "not-a-number"
response = authenticated_client.post(url, data)
assert response.status_code == status.HTTP_400_BAD_REQUEST
+assert "error" in response.data
+assert "Expiry must be a valid integer" in response.data["error"]
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2a5b1b0 and 03e791b.

📒 Files selected for processing (1)
  • backend/app_tests/api/test_api_personal_access_tokens.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
backend/app_tests/api/test_api_personal_access_tokens.py

8-8: knox.settings.knox_settings imported but unused

Remove unused import: knox.settings.knox_settings

(F401)


9-9: knox.auth.TokenAuthentication imported but unused

Remove unused import: knox.auth.TokenAuthentication

(F401)

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

104-121: Token limit assertion is correctly user-scoped.

Great improvement! The assertion now correctly filters by user to properly test the per-user token limit constraint, which aligns with the PR objective of limiting tokens to 5 per user.


222-265: Comprehensive test for expired token authentication.

This test thoroughly verifies that expired tokens are properly rejected for authentication, which is crucial for security. The test includes:

  1. Creating a token with a short expiry
  2. Verifying it works initially
  3. Manually expiring the token
  4. Verifying it no longer works

The detailed assertions and error messages make debugging failures easier.


95-97: Good validation for negative expiry values.

The test properly verifies that the API rejects negative expiry values with an appropriate error message.


145-180: Well-structured test for user isolation.

This test effectively ensures that the PAT implementation maintains proper user isolation, which is a critical security requirement. The test:

  1. Creates two distinct users
  2. Creates a token for each user
  3. Verifies each user can only see their own tokens

This validates that the API properly scopes token access to the authenticated user.

Copy link
Contributor

@ab-smith ab-smith left a comment

Choose a reason for hiding this comment

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

Functional ok

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: 1

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e862326 and 672ba44.

📒 Files selected for processing (2)
  • backend/core/views.py (3 hunks)
  • backend/fixtures/__init__.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/core/views.py (1)
backend/fixtures/__init__.py (1)
  • get_resource_path (7-17)
⏰ 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: functional-tests (3.12, chromium)
  • GitHub Check: test (3.12)
  • GitHub Check: build (3.12)
  • GitHub Check: enterprise-functional-tests (3.12, chromium)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
backend/fixtures/__init__.py (2)

1-2: LGTM: Appropriate imports for resource handling.

The imports are correct for modern Python resource handling using importlib.resources, which is the recommended approach for accessing package resources in a cross-platform manner.


4-4: LGTM: Proper package resource initialization.

The package resource reference is correctly initialized to point to the fixtures package using the modern importlib.resources.files() API.

backend/core/views.py (3)

34-34: LGTM: Clean import addition

The import of get_resource_path from the fixtures module is correctly placed and properly used in the import_dummy_domain method below.


135-135: LGTM: Modern resource handling import

The importlib.resources import is the correct choice for modern package resource access and is properly utilized in the refactored code.


2698-2745: Excellent refactoring with improved resource handling and error management

This refactoring significantly improves the import_dummy_domain method with several key enhancements:

Improvements:

  1. Modern Resource Access: Uses importlib.resources.as_file() context manager for safe resource handling
  2. Robust Error Handling: Comprehensive exception handling for different failure scenarios
  3. Resource Validation: Proper checking with is_file() before processing
  4. Enhanced Logging: Detailed logging for both success and error cases
  5. Clean Resource Management: Context manager ensures proper resource cleanup

Technical Benefits:

  • Follows modern Python best practices for package resource access
  • Eliminates potential resource leaks through proper context management
  • Provides clear error messages for different failure modes
  • Maintains backward compatibility while improving reliability

The implementation correctly handles the transition from a Traversable resource path to a concrete filesystem path, which is necessary for the existing file processing logic.

@ab-smith ab-smith merged commit d83ae19 into main May 23, 2025
22 checks passed
@ab-smith ab-smith deleted the feat/pat branch May 23, 2025 08:57
@github-actions github-actions bot locked and limited conversation to collaborators May 23, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants