Merge pull request #41 from bnema/feature/auth-status
feat(cli): add auth status command to verify session validity
27 files · +360 -44
M .mockery.yaml +2 -2
| @@ -7,7 +7,7 @@ structname: 'Mock{{.InterfaceName}}' | |||
| 7 | 7 | pkgname: 'mocks' | |
| 8 | 8 | ||
| 9 | 9 | packages: | |
| 10 | - | gordon/internal/boundaries/out: | |
| 10 | + | github.com/bnema/gordon/internal/boundaries/out: | |
| 11 | 11 | interfaces: | |
| 12 | 12 | ContainerRuntime: | |
| 13 | 13 | BlobStorage: | |
| @@ -22,7 +22,7 @@ packages: | |||
| 22 | 22 | TokenStore: | |
| 23 | 23 | DomainSecretStore: | |
| 24 | 24 | RateLimiter: | |
| 25 | - | gordon/internal/boundaries/in: | |
| 25 | + | github.com/bnema/gordon/internal/boundaries/in: | |
| 26 | 26 | interfaces: | |
| 27 | 27 | ContainerService: | |
| 28 | 28 | RegistryService: | |
| @@ -7,7 +7,7 @@ structname: 'Mock{{.InterfaceName}}' | |||||
| 7 | pkgname: 'mocks' | 7 | pkgname: 'mocks' | ||
| 8 | 8 | ||||
| 9 | packages: | 9 | packages: | ||
| 10 | - | gordon/internal/boundaries/out: | 10 | + | github.com/bnema/gordon/internal/boundaries/out: |
| 11 | interfaces: | 11 | interfaces: | ||
| 12 | ContainerRuntime: | 12 | ContainerRuntime: | ||
| 13 | BlobStorage: | 13 | BlobStorage: | ||
| @@ -22,7 +22,7 @@ packages: | |||||
| 22 | TokenStore: | 22 | TokenStore: | ||
| 23 | DomainSecretStore: | 23 | DomainSecretStore: | ||
| 24 | RateLimiter: | 24 | RateLimiter: | ||
| 25 | - | gordon/internal/boundaries/in: | 25 | + | github.com/bnema/gordon/internal/boundaries/in: |
| 26 | interfaces: | 26 | interfaces: | ||
| 27 | ContainerService: | 27 | ContainerService: | ||
| 28 | RegistryService: | 28 | RegistryService: | ||
A internal/adapters/dto/admin_auth.go +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // AuthVerifyResponse represents response from /admin/auth/verify. | |
| 4 | + | type AuthVerifyResponse struct { | |
| 5 | + | Valid bool `json:"valid"` | |
| 6 | + | Subject string `json:"subject,omitempty"` | |
| 7 | + | Scopes []string `json:"scopes,omitempty"` | |
| 8 | + | ExpiresAt int64 `json:"expires_at,omitempty"` | |
| 9 | + | IssuedAt int64 `json:"issued_at,omitempty"` | |
| 10 | + | } | |
| @@ -0,0 +1,10 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // AuthVerifyResponse represents response from /admin/auth/verify. | |||
| 4 | + | type AuthVerifyResponse struct { | |||
| 5 | + | Valid bool `json:"valid"` | |||
| 6 | + | Subject string `json:"subject,omitempty"` | |||
| 7 | + | Scopes []string `json:"scopes,omitempty"` | |||
| 8 | + | ExpiresAt int64 `json:"expires_at,omitempty"` | |||
| 9 | + | IssuedAt int64 `json:"issued_at,omitempty"` | |||
| 10 | + | } | |||
M internal/adapters/in/cli/auth.go +144 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/spf13/viper" | |
| 14 | 14 | "golang.org/x/term" | |
| 15 | 15 | ||
| 16 | + | "github.com/bnema/gordon/internal/adapters/dto" | |
| 16 | 17 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 17 | 18 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | |
| 18 | 19 | "github.com/bnema/gordon/internal/adapters/out/secrets" | |
| @@ -35,6 +36,7 @@ func newAuthCmd() *cobra.Command { | |||
| 35 | 36 | cmd.AddCommand(newAuthPasswordCmd()) | |
| 36 | 37 | cmd.AddCommand(newAuthInternalCmd()) | |
| 37 | 38 | cmd.AddCommand(newAuthLoginCmd()) | |
| 39 | + | cmd.AddCommand(newAuthStatusCmd()) | |
| 38 | 40 | ||
| 39 | 41 | return cmd | |
| 40 | 42 | } | |
| @@ -651,3 +653,145 @@ func createAuthServiceForCLI(cfg *cliConfig, log zerowrap.Logger) (*auth.Service | |||
| 651 | 653 | ||
| 652 | 654 | return auth.NewService(authConfig, store, log), nil | |
| 653 | 655 | } | |
| 656 | + | ||
| 657 | + | // newAuthStatusCmd creates the auth status command. | |
| 658 | + | func newAuthStatusCmd() *cobra.Command { | |
| 659 | + | var remoteNameFlag string | |
| 660 | + | cmd := &cobra.Command{ | |
| 661 | + | Use: "status", | |
| 662 | + | Short: "Check authentication session status", | |
| 663 | + | Long: `Verify if stored authentication session is still valid. | |
| 664 | + | ||
| 665 | + | Shows token validity, expiry, and connection status for the active remote. | |
| 666 | + | ||
| 667 | + | Examples: | |
| 668 | + | gordon auth status Check status of active remote | |
| 669 | + | gordon auth status --remote prod Check specific remote`, | |
| 670 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 671 | + | return runAuthStatus(remoteNameFlag) | |
| 672 | + | }, | |
| 673 | + | } | |
| 674 | + | cmd.Flags().StringVarP(&remoteNameFlag, "remote", "r", "", "Check specific remote") | |
| 675 | + | return cmd | |
| 676 | + | } | |
| 677 | + | ||
| 678 | + | // runAuthStatus checks authentication status for the active remote. | |
| 679 | + | func runAuthStatus(remoteNameArg string) error { | |
| 680 | + | // Load remotes config | |
| 681 | + | config, err := remote.LoadRemotes("") | |
| 682 | + | if err != nil { | |
| 683 | + | return fmt.Errorf("failed to load remotes: %w", err) | |
| 684 | + | } | |
| 685 | + | ||
| 686 | + | // Determine remote name | |
| 687 | + | remoteName := remoteNameArg | |
| 688 | + | if remoteName == "" { | |
| 689 | + | remoteName = config.Active | |
| 690 | + | } | |
| 691 | + | if remoteName == "" { | |
| 692 | + | return fmt.Errorf("no active remote configured. Use 'gordon remote use <name>' or --remote flag") | |
| 693 | + | } | |
| 694 | + | ||
| 695 | + | remoteConfig, ok := config.Remotes[remoteName] | |
| 696 | + | if !ok { | |
| 697 | + | if remoteNameArg != "" { | |
| 698 | + | return fmt.Errorf("remote '%s' not found", remoteName) | |
| 699 | + | } | |
| 700 | + | return fmt.Errorf("active remote '%s' not found", remoteName) | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | // Create client | |
| 704 | + | client := remote.NewClient(remoteConfig.URL, remote.WithToken(remoteConfig.Token)) | |
| 705 | + | ||
| 706 | + | // Verify auth | |
| 707 | + | ctx := context.Background() | |
| 708 | + | fmt.Printf("Checking authentication status for '%s'...\n", remoteName) | |
| 709 | + | ||
| 710 | + | status, err := client.VerifyAuth(ctx) | |
| 711 | + | if err != nil { | |
| 712 | + | return handleAuthVerifyError(err, remoteName, remoteConfig.URL) | |
| 713 | + | } | |
| 714 | + | ||
| 715 | + | // Display status | |
| 716 | + | displayAuthStatus(remoteName, remoteConfig.URL, status) | |
| 717 | + | return nil | |
| 718 | + | } | |
| 719 | + | ||
| 720 | + | // handleAuthVerifyError handles errors from auth verification. | |
| 721 | + | func handleAuthVerifyError(err error, remoteName, url string) error { | |
| 722 | + | errStrLower := strings.ToLower(err.Error()) | |
| 723 | + | ||
| 724 | + | // Connection errors | |
| 725 | + | if strings.Contains(errStrLower, "connection refused") || | |
| 726 | + | strings.Contains(errStrLower, "no such host") || | |
| 727 | + | strings.Contains(errStrLower, "timeout") { | |
| 728 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |
| 729 | + | fmt.Println() | |
| 730 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Connection Failed")) | |
| 731 | + | fmt.Printf("Error: %s\n\n", err.Error()) | |
| 732 | + | fmt.Println(styles.Theme.Muted.Render("Check that Gordon server is running and accessible.")) | |
| 733 | + | return nil | |
| 734 | + | } | |
| 735 | + | ||
| 736 | + | // Auth errors | |
| 737 | + | if strings.Contains(errStrLower, "401") || | |
| 738 | + | strings.Contains(errStrLower, "unauthorized") || | |
| 739 | + | strings.Contains(errStrLower, "invalid token") { | |
| 740 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |
| 741 | + | fmt.Println() | |
| 742 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Authentication Failed")) | |
| 743 | + | fmt.Println() | |
| 744 | + | fmt.Println(styles.Theme.Muted.Render("Reason: Token is invalid or expired")) | |
| 745 | + | fmt.Println() | |
| 746 | + | fmt.Println(styles.Theme.Bold.Render("Action: Re-authenticate with:")) | |
| 747 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |
| 748 | + | return nil | |
| 749 | + | } | |
| 750 | + | ||
| 751 | + | return fmt.Errorf("failed to verify auth: %w", err) | |
| 752 | + | } | |
| 753 | + | ||
| 754 | + | // displayAuthStatus displays authentication status information. | |
| 755 | + | func displayAuthStatus(remoteName, url string, status *dto.AuthVerifyResponse) { | |
| 756 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + remoteName + " | URL: " + url)) | |
| 757 | + | fmt.Println() | |
| 758 | + | ||
| 759 | + | if status.Valid { | |
| 760 | + | // Valid authentication | |
| 761 | + | fmt.Println(styles.Theme.Success.Render(styles.IconSuccess + " Authenticated")) | |
| 762 | + | fmt.Println() | |
| 763 | + | ||
| 764 | + | if status.Subject != "" { | |
| 765 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Subject:"), status.Subject) | |
| 766 | + | } | |
| 767 | + | ||
| 768 | + | if status.IssuedAt > 0 { | |
| 769 | + | issuedAt := time.Unix(status.IssuedAt, 0) | |
| 770 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Issued:"), issuedAt.Format("2006-01-02 15:04:05")) | |
| 771 | + | } | |
| 772 | + | ||
| 773 | + | if status.ExpiresAt > 0 { | |
| 774 | + | expiresAt := time.Unix(status.ExpiresAt, 0) | |
| 775 | + | if expiresAt.Before(time.Now()) { | |
| 776 | + | fmt.Printf("%s %s (%s ago)\n", styles.Theme.Bold.Render("Expired:"), | |
| 777 | + | expiresAt.Format("2006-01-02 15:04:05"), | |
| 778 | + | styles.Theme.Muted.Render(time.Since(expiresAt).Round(time.Second).String())) | |
| 779 | + | } else { | |
| 780 | + | remaining := time.Until(expiresAt) | |
| 781 | + | fmt.Printf("%s %s (%s)\n", styles.Theme.Bold.Render("Expires:"), | |
| 782 | + | expiresAt.Format("2006-01-02 15:04:05"), | |
| 783 | + | styles.Theme.Muted.Render(remaining.Round(time.Second).String())) | |
| 784 | + | } | |
| 785 | + | } | |
| 786 | + | ||
| 787 | + | if len(status.Scopes) > 0 { | |
| 788 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Scopes:"), strings.Join(status.Scopes, ", ")) | |
| 789 | + | } | |
| 790 | + | } else { | |
| 791 | + | // Invalid or no authentication | |
| 792 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Not Authenticated")) | |
| 793 | + | fmt.Println() | |
| 794 | + | fmt.Println(styles.Theme.Muted.Render("Action: Re-authenticate with:")) | |
| 795 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |
| 796 | + | } | |
| 797 | + | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/spf13/viper" | 13 | "github.com/spf13/viper" | ||
| 14 | "golang.org/x/term" | 14 | "golang.org/x/term" | ||
| 15 | 15 | ||||
| 16 | + | "github.com/bnema/gordon/internal/adapters/dto" | |||
| 16 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 17 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 17 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | 18 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | ||
| 18 | "github.com/bnema/gordon/internal/adapters/out/secrets" | 19 | "github.com/bnema/gordon/internal/adapters/out/secrets" | ||
| @@ -35,6 +36,7 @@ func newAuthCmd() *cobra.Command { | |||||
| 35 | cmd.AddCommand(newAuthPasswordCmd()) | 36 | cmd.AddCommand(newAuthPasswordCmd()) | ||
| 36 | cmd.AddCommand(newAuthInternalCmd()) | 37 | cmd.AddCommand(newAuthInternalCmd()) | ||
| 37 | cmd.AddCommand(newAuthLoginCmd()) | 38 | cmd.AddCommand(newAuthLoginCmd()) | ||
| 39 | + | cmd.AddCommand(newAuthStatusCmd()) | |||
| 38 | 40 | ||||
| 39 | return cmd | 41 | return cmd | ||
| 40 | } | 42 | } | ||
| @@ -651,3 +653,145 @@ func createAuthServiceForCLI(cfg *cliConfig, log zerowrap.Logger) (*auth.Service | |||||
| 651 | 653 | ||||
| 652 | return auth.NewService(authConfig, store, log), nil | 654 | return auth.NewService(authConfig, store, log), nil | ||
| 653 | } | 655 | } | ||
| 656 | + | ||||
| 657 | + | // newAuthStatusCmd creates the auth status command. | |||
| 658 | + | func newAuthStatusCmd() *cobra.Command { | |||
| 659 | + | var remoteNameFlag string | |||
| 660 | + | cmd := &cobra.Command{ | |||
| 661 | + | Use: "status", | |||
| 662 | + | Short: "Check authentication session status", | |||
| 663 | + | Long: `Verify if stored authentication session is still valid. | |||
| 664 | + | ||||
| 665 | + | Shows token validity, expiry, and connection status for the active remote. | |||
| 666 | + | ||||
| 667 | + | Examples: | |||
| 668 | + | gordon auth status Check status of active remote | |||
| 669 | + | gordon auth status --remote prod Check specific remote`, | |||
| 670 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 671 | + | return runAuthStatus(remoteNameFlag) | |||
| 672 | + | }, | |||
| 673 | + | } | |||
| 674 | + | cmd.Flags().StringVarP(&remoteNameFlag, "remote", "r", "", "Check specific remote") | |||
| 675 | + | return cmd | |||
| 676 | + | } | |||
| 677 | + | ||||
| 678 | + | // runAuthStatus checks authentication status for the active remote. | |||
| 679 | + | func runAuthStatus(remoteNameArg string) error { | |||
| 680 | + | // Load remotes config | |||
| 681 | + | config, err := remote.LoadRemotes("") | |||
| 682 | + | if err != nil { | |||
| 683 | + | return fmt.Errorf("failed to load remotes: %w", err) | |||
| 684 | + | } | |||
| 685 | + | ||||
| 686 | + | // Determine remote name | |||
| 687 | + | remoteName := remoteNameArg | |||
| 688 | + | if remoteName == "" { | |||
| 689 | + | remoteName = config.Active | |||
| 690 | + | } | |||
| 691 | + | if remoteName == "" { | |||
| 692 | + | return fmt.Errorf("no active remote configured. Use 'gordon remote use <name>' or --remote flag") | |||
| 693 | + | } | |||
| 694 | + | ||||
| 695 | + | remoteConfig, ok := config.Remotes[remoteName] | |||
| 696 | + | if !ok { | |||
| 697 | + | if remoteNameArg != "" { | |||
| 698 | + | return fmt.Errorf("remote '%s' not found", remoteName) | |||
| 699 | + | } | |||
| 700 | + | return fmt.Errorf("active remote '%s' not found", remoteName) | |||
| 701 | + | } | |||
| 702 | + | ||||
| 703 | + | // Create client | |||
| 704 | + | client := remote.NewClient(remoteConfig.URL, remote.WithToken(remoteConfig.Token)) | |||
| 705 | + | ||||
| 706 | + | // Verify auth | |||
| 707 | + | ctx := context.Background() | |||
| 708 | + | fmt.Printf("Checking authentication status for '%s'...\n", remoteName) | |||
| 709 | + | ||||
| 710 | + | status, err := client.VerifyAuth(ctx) | |||
| 711 | + | if err != nil { | |||
| 712 | + | return handleAuthVerifyError(err, remoteName, remoteConfig.URL) | |||
| 713 | + | } | |||
| 714 | + | ||||
| 715 | + | // Display status | |||
| 716 | + | displayAuthStatus(remoteName, remoteConfig.URL, status) | |||
| 717 | + | return nil | |||
| 718 | + | } | |||
| 719 | + | ||||
| 720 | + | // handleAuthVerifyError handles errors from auth verification. | |||
| 721 | + | func handleAuthVerifyError(err error, remoteName, url string) error { | |||
| 722 | + | errStrLower := strings.ToLower(err.Error()) | |||
| 723 | + | ||||
| 724 | + | // Connection errors | |||
| 725 | + | if strings.Contains(errStrLower, "connection refused") || | |||
| 726 | + | strings.Contains(errStrLower, "no such host") || | |||
| 727 | + | strings.Contains(errStrLower, "timeout") { | |||
| 728 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |||
| 729 | + | fmt.Println() | |||
| 730 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Connection Failed")) | |||
| 731 | + | fmt.Printf("Error: %s\n\n", err.Error()) | |||
| 732 | + | fmt.Println(styles.Theme.Muted.Render("Check that Gordon server is running and accessible.")) | |||
| 733 | + | return nil | |||
| 734 | + | } | |||
| 735 | + | ||||
| 736 | + | // Auth errors | |||
| 737 | + | if strings.Contains(errStrLower, "401") || | |||
| 738 | + | strings.Contains(errStrLower, "unauthorized") || | |||
| 739 | + | strings.Contains(errStrLower, "invalid token") { | |||
| 740 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |||
| 741 | + | fmt.Println() | |||
| 742 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Authentication Failed")) | |||
| 743 | + | fmt.Println() | |||
| 744 | + | fmt.Println(styles.Theme.Muted.Render("Reason: Token is invalid or expired")) | |||
| 745 | + | fmt.Println() | |||
| 746 | + | fmt.Println(styles.Theme.Bold.Render("Action: Re-authenticate with:")) | |||
| 747 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |||
| 748 | + | return nil | |||
| 749 | + | } | |||
| 750 | + | ||||
| 751 | + | return fmt.Errorf("failed to verify auth: %w", err) | |||
| 752 | + | } | |||
| 753 | + | ||||
| 754 | + | // displayAuthStatus displays authentication status information. | |||
| 755 | + | func displayAuthStatus(remoteName, url string, status *dto.AuthVerifyResponse) { | |||
| 756 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + remoteName + " | URL: " + url)) | |||
| 757 | + | fmt.Println() | |||
| 758 | + | ||||
| 759 | + | if status.Valid { | |||
| 760 | + | // Valid authentication | |||
| 761 | + | fmt.Println(styles.Theme.Success.Render(styles.IconSuccess + " Authenticated")) | |||
| 762 | + | fmt.Println() | |||
| 763 | + | ||||
| 764 | + | if status.Subject != "" { | |||
| 765 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Subject:"), status.Subject) | |||
| 766 | + | } | |||
| 767 | + | ||||
| 768 | + | if status.IssuedAt > 0 { | |||
| 769 | + | issuedAt := time.Unix(status.IssuedAt, 0) | |||
| 770 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Issued:"), issuedAt.Format("2006-01-02 15:04:05")) | |||
| 771 | + | } | |||
| 772 | + | ||||
| 773 | + | if status.ExpiresAt > 0 { | |||
| 774 | + | expiresAt := time.Unix(status.ExpiresAt, 0) | |||
| 775 | + | if expiresAt.Before(time.Now()) { | |||
| 776 | + | fmt.Printf("%s %s (%s ago)\n", styles.Theme.Bold.Render("Expired:"), | |||
| 777 | + | expiresAt.Format("2006-01-02 15:04:05"), | |||
| 778 | + | styles.Theme.Muted.Render(time.Since(expiresAt).Round(time.Second).String())) | |||
| 779 | + | } else { | |||
| 780 | + | remaining := time.Until(expiresAt) | |||
| 781 | + | fmt.Printf("%s %s (%s)\n", styles.Theme.Bold.Render("Expires:"), | |||
| 782 | + | expiresAt.Format("2006-01-02 15:04:05"), | |||
| 783 | + | styles.Theme.Muted.Render(remaining.Round(time.Second).String())) | |||
| 784 | + | } | |||
| 785 | + | } | |||
| 786 | + | ||||
| 787 | + | if len(status.Scopes) > 0 { | |||
| 788 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Scopes:"), strings.Join(status.Scopes, ", ")) | |||
| 789 | + | } | |||
| 790 | + | } else { | |||
| 791 | + | // Invalid or no authentication | |||
| 792 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Not Authenticated")) | |||
| 793 | + | fmt.Println() | |||
| 794 | + | fmt.Println(styles.Theme.Muted.Render("Action: Re-authenticate with:")) | |||
| 795 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |||
| 796 | + | } | |||
| 797 | + | } | |||
M internal/adapters/in/cli/remote/client.go +15 -0
| @@ -632,3 +632,18 @@ func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, er | |||
| 632 | 632 | ||
| 633 | 633 | return ch, nil | |
| 634 | 634 | } | |
| 635 | + | ||
| 636 | + | // VerifyAuth checks if authentication session is valid. | |
| 637 | + | func (c *Client) VerifyAuth(ctx context.Context) (*dto.AuthVerifyResponse, error) { | |
| 638 | + | resp, err := c.request(ctx, http.MethodGet, "/auth/verify", nil) | |
| 639 | + | if err != nil { | |
| 640 | + | return nil, err | |
| 641 | + | } | |
| 642 | + | ||
| 643 | + | var result dto.AuthVerifyResponse | |
| 644 | + | if err := parseResponse(resp, &result); err != nil { | |
| 645 | + | return nil, err | |
| 646 | + | } | |
| 647 | + | ||
| 648 | + | return &result, nil | |
| 649 | + | } | |
| @@ -632,3 +632,18 @@ func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, er | |||||
| 632 | 632 | ||||
| 633 | return ch, nil | 633 | return ch, nil | ||
| 634 | } | 634 | } | ||
| 635 | + | ||||
| 636 | + | // VerifyAuth checks if authentication session is valid. | |||
| 637 | + | func (c *Client) VerifyAuth(ctx context.Context) (*dto.AuthVerifyResponse, error) { | |||
| 638 | + | resp, err := c.request(ctx, http.MethodGet, "/auth/verify", nil) | |||
| 639 | + | if err != nil { | |||
| 640 | + | return nil, err | |||
| 641 | + | } | |||
| 642 | + | ||||
| 643 | + | var result dto.AuthVerifyResponse | |||
| 644 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 645 | + | return nil, err | |||
| 646 | + | } | |||
| 647 | + | ||||
| 648 | + | return &result, nil | |||
| 649 | + | } | |||
M internal/adapters/in/http/admin/handler.go +37 -5
| @@ -140,11 +140,12 @@ type routeHandler func(w http.ResponseWriter, r *http.Request, path string) | |||
| 140 | 140 | func (h *Handler) matchRoute(path string) (routeHandler, bool) { | |
| 141 | 141 | // Exact match routes | |
| 142 | 142 | exactRoutes := map[string]routeHandler{ | |
| 143 | - | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, | |
| 144 | - | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, | |
| 145 | - | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, | |
| 146 | - | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, | |
| 147 | - | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, | |
| 143 | + | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, | |
| 144 | + | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, | |
| 145 | + | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, | |
| 146 | + | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, | |
| 147 | + | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, | |
| 148 | + | "/auth/verify": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAuthVerify(w, r) }, | |
| 148 | 149 | } | |
| 149 | 150 | if handler, ok := exactRoutes[path]; ok { | |
| 150 | 151 | return handler, true | |
| @@ -1078,3 +1079,34 @@ func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.R | |||
| 1078 | 1079 | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | |
| 1079 | 1080 | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | |
| 1080 | 1081 | } | |
| 1082 | + | ||
| 1083 | + | // handleAuthVerify handles /admin/auth/verify endpoint. | |
| 1084 | + | // Validates authentication session and returns token status. | |
| 1085 | + | func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) { | |
| 1086 | + | ctx := r.Context() | |
| 1087 | + | ||
| 1088 | + | // Only allow GET method | |
| 1089 | + | if r.Method != http.MethodGet { | |
| 1090 | + | w.Header().Set("Allow", http.MethodGet) | |
| 1091 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 1092 | + | return | |
| 1093 | + | } | |
| 1094 | + | ||
| 1095 | + | // Call use case to get auth status | |
| 1096 | + | status, err := h.authSvc.GetAuthStatus(ctx) | |
| 1097 | + | if err != nil { | |
| 1098 | + | h.sendError(w, http.StatusInternalServerError, "failed to get auth status") | |
| 1099 | + | return | |
| 1100 | + | } | |
| 1101 | + | ||
| 1102 | + | // Convert domain.AuthStatus to DTO | |
| 1103 | + | response := dto.AuthVerifyResponse{ | |
| 1104 | + | Valid: status.Valid, | |
| 1105 | + | Subject: status.Subject, | |
| 1106 | + | Scopes: status.Scopes, | |
| 1107 | + | ExpiresAt: status.ExpiresAt, | |
| 1108 | + | IssuedAt: status.IssuedAt, | |
| 1109 | + | } | |
| 1110 | + | ||
| 1111 | + | h.sendJSON(w, http.StatusOK, response) | |
| 1112 | + | } | |
| @@ -140,11 +140,12 @@ type routeHandler func(w http.ResponseWriter, r *http.Request, path string) | |||||
| 140 | func (h *Handler) matchRoute(path string) (routeHandler, bool) { | 140 | func (h *Handler) matchRoute(path string) (routeHandler, bool) { | ||
| 141 | // Exact match routes | 141 | // Exact match routes | ||
| 142 | exactRoutes := map[string]routeHandler{ | 142 | exactRoutes := map[string]routeHandler{ | ||
| 143 | - | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, | 143 | + | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, |
| 144 | - | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, | 144 | + | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, |
| 145 | - | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, | 145 | + | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, |
| 146 | - | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, | 146 | + | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, |
| 147 | - | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, | 147 | + | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, |
| 148 | + | "/auth/verify": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAuthVerify(w, r) }, | |||
| 148 | } | 149 | } | ||
| 149 | if handler, ok := exactRoutes[path]; ok { | 150 | if handler, ok := exactRoutes[path]; ok { | ||
| 150 | return handler, true | 151 | return handler, true | ||
| @@ -1078,3 +1079,34 @@ func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.R | |||||
| 1078 | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | 1079 | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | ||
| 1079 | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | 1080 | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | ||
| 1080 | } | 1081 | } | ||
| 1082 | + | ||||
| 1083 | + | // handleAuthVerify handles /admin/auth/verify endpoint. | |||
| 1084 | + | // Validates authentication session and returns token status. | |||
| 1085 | + | func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) { | |||
| 1086 | + | ctx := r.Context() | |||
| 1087 | + | ||||
| 1088 | + | // Only allow GET method | |||
| 1089 | + | if r.Method != http.MethodGet { | |||
| 1090 | + | w.Header().Set("Allow", http.MethodGet) | |||
| 1091 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |||
| 1092 | + | return | |||
| 1093 | + | } | |||
| 1094 | + | ||||
| 1095 | + | // Call use case to get auth status | |||
| 1096 | + | status, err := h.authSvc.GetAuthStatus(ctx) | |||
| 1097 | + | if err != nil { | |||
| 1098 | + | h.sendError(w, http.StatusInternalServerError, "failed to get auth status") | |||
| 1099 | + | return | |||
| 1100 | + | } | |||
| 1101 | + | ||||
| 1102 | + | // Convert domain.AuthStatus to DTO | |||
| 1103 | + | response := dto.AuthVerifyResponse{ | |||
| 1104 | + | Valid: status.Valid, | |||
| 1105 | + | Subject: status.Subject, | |||
| 1106 | + | Scopes: status.Scopes, | |||
| 1107 | + | ExpiresAt: status.ExpiresAt, | |||
| 1108 | + | IssuedAt: status.IssuedAt, | |||
| 1109 | + | } | |||
| 1110 | + | ||||
| 1111 | + | h.sendJSON(w, http.StatusOK, response) | |||
| 1112 | + | } | |||
M internal/adapters/in/http/admin/handler_test.go +1 -1
| @@ -23,7 +23,7 @@ func testLogger() zerowrap.Logger { | |||
| 23 | 23 | ||
| 24 | 24 | func ctxWithScopes(scopes ...string) context.Context { | |
| 25 | 25 | ctx := context.Background() | |
| 26 | - | return context.WithValue(ctx, ContextKeyScopes, scopes) | |
| 26 | + | return context.WithValue(ctx, domain.ContextKeyScopes, scopes) | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | 29 | // Routes endpoint tests | |
| @@ -23,7 +23,7 @@ func testLogger() zerowrap.Logger { | |||||
| 23 | 23 | ||||
| 24 | func ctxWithScopes(scopes ...string) context.Context { | 24 | func ctxWithScopes(scopes ...string) context.Context { | ||
| 25 | ctx := context.Background() | 25 | ctx := context.Background() | ||
| 26 | - | return context.WithValue(ctx, ContextKeyScopes, scopes) | 26 | + | return context.WithValue(ctx, domain.ContextKeyScopes, scopes) |
| 27 | } | 27 | } | ||
| 28 | 28 | ||||
| 29 | // Routes endpoint tests | 29 | // Routes endpoint tests | ||
M internal/adapters/in/http/admin/middleware.go +6 -15
| @@ -16,16 +16,6 @@ import ( | |||
| 16 | 16 | "github.com/bnema/gordon/internal/domain" | |
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | - | // contextKey is a custom type for context keys to avoid collisions. | |
| 20 | - | type contextKey string | |
| 21 | - | ||
| 22 | - | const ( | |
| 23 | - | // ContextKeyScopes is the context key for storing token scopes. | |
| 24 | - | ContextKeyScopes contextKey = "admin_scopes" | |
| 25 | - | // ContextKeySubject is the context key for storing token subject. | |
| 26 | - | ContextKeySubject contextKey = "admin_subject" | |
| 27 | - | ) | |
| 28 | - | ||
| 29 | 19 | // AuthMiddleware creates middleware that validates admin API authentication. | |
| 30 | 20 | // Both globalLimiter and ipLimiter can be nil to disable rate limiting. | |
| 31 | 21 | // The trustedNets parameter is used for proper IP extraction behind reverse proxies. | |
| @@ -110,8 +100,9 @@ func AuthMiddleware( | |||
| 110 | 100 | } | |
| 111 | 101 | ||
| 112 | 102 | // Add claims to context for downstream handlers | |
| 113 | - | ctx = context.WithValue(ctx, ContextKeyScopes, claims.Scopes) | |
| 114 | - | ctx = context.WithValue(ctx, ContextKeySubject, claims.Subject) | |
| 103 | + | ctx = context.WithValue(ctx, domain.ContextKeyScopes, claims.Scopes) | |
| 104 | + | ctx = context.WithValue(ctx, domain.ContextKeySubject, claims.Subject) | |
| 105 | + | ctx = context.WithValue(ctx, domain.TokenClaimsKey, claims) | |
| 115 | 106 | ||
| 116 | 107 | next.ServeHTTP(w, r.WithContext(ctx)) | |
| 117 | 108 | }) | |
| @@ -125,7 +116,7 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler { | |||
| 125 | 116 | ctx := r.Context() | |
| 126 | 117 | ||
| 127 | 118 | // Get scopes from context | |
| 128 | - | scopes, ok := ctx.Value(ContextKeyScopes).([]string) | |
| 119 | + | scopes, ok := ctx.Value(domain.ContextKeyScopes).([]string) | |
| 129 | 120 | if !ok { | |
| 130 | 121 | sendForbidden(w, "no scopes in context") | |
| 131 | 122 | return | |
| @@ -144,13 +135,13 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler { | |||
| 144 | 135 | ||
| 145 | 136 | // GetSubject retrieves the authenticated subject from the context. | |
| 146 | 137 | func GetSubject(ctx context.Context) string { | |
| 147 | - | subject, _ := ctx.Value(ContextKeySubject).(string) | |
| 138 | + | subject, _ := ctx.Value(domain.ContextKeySubject).(string) | |
| 148 | 139 | return subject | |
| 149 | 140 | } | |
| 150 | 141 | ||
| 151 | 142 | // GetScopes retrieves the token scopes from the context. | |
| 152 | 143 | func GetScopes(ctx context.Context) []string { | |
| 153 | - | scopes, _ := ctx.Value(ContextKeyScopes).([]string) | |
| 144 | + | scopes, _ := ctx.Value(domain.ContextKeyScopes).([]string) | |
| 154 | 145 | return scopes | |
| 155 | 146 | } | |
| 156 | 147 | ||
| @@ -16,16 +16,6 @@ import ( | |||||
| 16 | "github.com/bnema/gordon/internal/domain" | 16 | "github.com/bnema/gordon/internal/domain" | ||
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | - | // contextKey is a custom type for context keys to avoid collisions. | |||
| 20 | - | type contextKey string | |||
| 21 | - | ||||
| 22 | - | const ( | |||
| 23 | - | // ContextKeyScopes is the context key for storing token scopes. | |||
| 24 | - | ContextKeyScopes contextKey = "admin_scopes" | |||
| 25 | - | // ContextKeySubject is the context key for storing token subject. | |||
| 26 | - | ContextKeySubject contextKey = "admin_subject" | |||
| 27 | - | ) | |||
| 28 | - | ||||
| 29 | // AuthMiddleware creates middleware that validates admin API authentication. | 19 | // AuthMiddleware creates middleware that validates admin API authentication. | ||
| 30 | // Both globalLimiter and ipLimiter can be nil to disable rate limiting. | 20 | // Both globalLimiter and ipLimiter can be nil to disable rate limiting. | ||
| 31 | // The trustedNets parameter is used for proper IP extraction behind reverse proxies. | 21 | // The trustedNets parameter is used for proper IP extraction behind reverse proxies. | ||
| @@ -110,8 +100,9 @@ func AuthMiddleware( | |||||
| 110 | } | 100 | } | ||
| 111 | 101 | ||||
| 112 | // Add claims to context for downstream handlers | 102 | // Add claims to context for downstream handlers | ||
| 113 | - | ctx = context.WithValue(ctx, ContextKeyScopes, claims.Scopes) | 103 | + | ctx = context.WithValue(ctx, domain.ContextKeyScopes, claims.Scopes) |
| 114 | - | ctx = context.WithValue(ctx, ContextKeySubject, claims.Subject) | 104 | + | ctx = context.WithValue(ctx, domain.ContextKeySubject, claims.Subject) |
| 105 | + | ctx = context.WithValue(ctx, domain.TokenClaimsKey, claims) | |||
| 115 | 106 | ||||
| 116 | next.ServeHTTP(w, r.WithContext(ctx)) | 107 | next.ServeHTTP(w, r.WithContext(ctx)) | ||
| 117 | }) | 108 | }) | ||
| @@ -125,7 +116,7 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler { | |||||
| 125 | ctx := r.Context() | 116 | ctx := r.Context() | ||
| 126 | 117 | ||||
| 127 | // Get scopes from context | 118 | // Get scopes from context | ||
| 128 | - | scopes, ok := ctx.Value(ContextKeyScopes).([]string) | 119 | + | scopes, ok := ctx.Value(domain.ContextKeyScopes).([]string) |
| 129 | if !ok { | 120 | if !ok { | ||
| 130 | sendForbidden(w, "no scopes in context") | 121 | sendForbidden(w, "no scopes in context") | ||
| 131 | return | 122 | return | ||
| @@ -144,13 +135,13 @@ func RequireScope(resource, action string) func(http.Handler) http.Handler { | |||||
| 144 | 135 | ||||
| 145 | // GetSubject retrieves the authenticated subject from the context. | 136 | // GetSubject retrieves the authenticated subject from the context. | ||
| 146 | func GetSubject(ctx context.Context) string { | 137 | func GetSubject(ctx context.Context) string { | ||
| 147 | - | subject, _ := ctx.Value(ContextKeySubject).(string) | 138 | + | subject, _ := ctx.Value(domain.ContextKeySubject).(string) |
| 148 | return subject | 139 | return subject | ||
| 149 | } | 140 | } | ||
| 150 | 141 | ||||
| 151 | // GetScopes retrieves the token scopes from the context. | 142 | // GetScopes retrieves the token scopes from the context. | ||
| 152 | func GetScopes(ctx context.Context) []string { | 143 | func GetScopes(ctx context.Context) []string { | ||
| 153 | - | scopes, _ := ctx.Value(ContextKeyScopes).([]string) | 144 | + | scopes, _ := ctx.Value(domain.ContextKeyScopes).([]string) |
| 154 | return scopes | 145 | return scopes | ||
| 155 | } | 146 | } | ||
| 156 | 147 | ||||
M internal/adapters/in/http/admin/middleware_test.go +2 -2
| @@ -340,7 +340,7 @@ func TestRequireScope(t *testing.T) { | |||
| 340 | 340 | ||
| 341 | 341 | req := httptest.NewRequest(http.MethodGet, "/admin/routes", nil) | |
| 342 | 342 | // Add scopes to context | |
| 343 | - | ctx := context.WithValue(req.Context(), ContextKeyScopes, tt.scopes) | |
| 343 | + | ctx := context.WithValue(req.Context(), domain.ContextKeyScopes, tt.scopes) | |
| 344 | 344 | req = req.WithContext(ctx) | |
| 345 | 345 | ||
| 346 | 346 | rec := httptest.NewRecorder() | |
| @@ -352,7 +352,7 @@ func TestRequireScope(t *testing.T) { | |||
| 352 | 352 | } | |
| 353 | 353 | ||
| 354 | 354 | func TestHasAccess(t *testing.T) { | |
| 355 | - | ctx := context.WithValue(context.Background(), ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"}) | |
| 355 | + | ctx := context.WithValue(context.Background(), domain.ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"}) | |
| 356 | 356 | ||
| 357 | 357 | assert.True(t, HasAccess(ctx, "routes", "read")) | |
| 358 | 358 | assert.False(t, HasAccess(ctx, "routes", "write")) | |
| @@ -340,7 +340,7 @@ func TestRequireScope(t *testing.T) { | |||||
| 340 | 340 | ||||
| 341 | req := httptest.NewRequest(http.MethodGet, "/admin/routes", nil) | 341 | req := httptest.NewRequest(http.MethodGet, "/admin/routes", nil) | ||
| 342 | // Add scopes to context | 342 | // Add scopes to context | ||
| 343 | - | ctx := context.WithValue(req.Context(), ContextKeyScopes, tt.scopes) | 343 | + | ctx := context.WithValue(req.Context(), domain.ContextKeyScopes, tt.scopes) |
| 344 | req = req.WithContext(ctx) | 344 | req = req.WithContext(ctx) | ||
| 345 | 345 | ||||
| 346 | rec := httptest.NewRecorder() | 346 | rec := httptest.NewRecorder() | ||
| @@ -352,7 +352,7 @@ func TestRequireScope(t *testing.T) { | |||||
| 352 | } | 352 | } | ||
| 353 | 353 | ||||
| 354 | func TestHasAccess(t *testing.T) { | 354 | func TestHasAccess(t *testing.T) { | ||
| 355 | - | ctx := context.WithValue(context.Background(), ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"}) | 355 | + | ctx := context.WithValue(context.Background(), domain.ContextKeyScopes, []string{"admin:routes:read", "admin:secrets:*"}) |
| 356 | 356 | ||||
| 357 | assert.True(t, HasAccess(ctx, "routes", "read")) | 357 | assert.True(t, HasAccess(ctx, "routes", "read")) | ||
| 358 | assert.False(t, HasAccess(ctx, "routes", "write")) | 358 | assert.False(t, HasAccess(ctx, "routes", "write")) | ||
M internal/adapters/in/http/middleware/auth.go +3 -5
| @@ -14,11 +14,9 @@ import ( | |||
| 14 | 14 | "github.com/bnema/gordon/internal/domain" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | - | // contextKey is a type for context keys used by this package. | |
| 18 | - | type contextKey string | |
| 19 | - | ||
| 20 | - | // TokenClaimsKey is the context key for storing token claims. | |
| 21 | - | const TokenClaimsKey contextKey = "tokenClaims" | |
| 17 | + | // TokenClaimsKey is context key for storing token claims. | |
| 18 | + | // Using domain key for consistency across all auth flows. | |
| 19 | + | const TokenClaimsKey = domain.TokenClaimsKey | |
| 22 | 20 | ||
| 23 | 21 | // RegistryAuth middleware provides Docker Registry authentication. | |
| 24 | 22 | func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler { | |
| @@ -14,11 +14,9 @@ import ( | |||||
| 14 | "github.com/bnema/gordon/internal/domain" | 14 | "github.com/bnema/gordon/internal/domain" | ||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | - | // contextKey is a type for context keys used by this package. | 17 | + | // TokenClaimsKey is context key for storing token claims. |
| 18 | - | type contextKey string | 18 | + | // Using domain key for consistency across all auth flows. |
| 19 | - | 19 | + | const TokenClaimsKey = domain.TokenClaimsKey | |
| 20 | - | // TokenClaimsKey is the context key for storing token claims. | |||
| 21 | - | const TokenClaimsKey contextKey = "tokenClaims" | |||
| 22 | 20 | ||||
| 23 | // RegistryAuth middleware provides Docker Registry authentication. | 21 | // RegistryAuth middleware provides Docker Registry authentication. | ||
| 24 | func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler { | 22 | func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler { | ||
M internal/adapters/in/http/middleware/auth_test.go +4 -0
| @@ -501,6 +501,10 @@ func (s stubAuthService) GeneratePasswordHash(string) (string, error) { | |||
| 501 | 501 | return "", errors.New("not implemented") | |
| 502 | 502 | } | |
| 503 | 503 | ||
| 504 | + | func (s stubAuthService) GetAuthStatus(context.Context) (*domain.AuthStatus, error) { | |
| 505 | + | return nil, errors.New("not implemented") | |
| 506 | + | } | |
| 507 | + | ||
| 504 | 508 | // Tests for checkScopeAccess function | |
| 505 | 509 | ||
| 506 | 510 | func TestCheckScopeAccess_ActionMapping(t *testing.T) { | |
| @@ -501,6 +501,10 @@ func (s stubAuthService) GeneratePasswordHash(string) (string, error) { | |||||
| 501 | return "", errors.New("not implemented") | 501 | return "", errors.New("not implemented") | ||
| 502 | } | 502 | } | ||
| 503 | 503 | ||||
| 504 | + | func (s stubAuthService) GetAuthStatus(context.Context) (*domain.AuthStatus, error) { | |||
| 505 | + | return nil, errors.New("not implemented") | |||
| 506 | + | } | |||
| 507 | + | ||||
| 504 | // Tests for checkScopeAccess function | 508 | // Tests for checkScopeAccess function | ||
| 505 | 509 | ||||
| 506 | func TestCheckScopeAccess_ActionMapping(t *testing.T) { | 510 | func TestCheckScopeAccess_ActionMapping(t *testing.T) { | ||
M internal/boundaries/in/auth.go +4 -0
| @@ -42,4 +42,8 @@ type AuthService interface { | |||
| 42 | 42 | ||
| 43 | 43 | // GeneratePasswordHash generates a bcrypt hash for a password. | |
| 44 | 44 | GeneratePasswordHash(password string) (string, error) | |
| 45 | + | ||
| 46 | + | // GetAuthStatus returns authentication status from context. | |
| 47 | + | // Extracts and validates token claims that were set by auth middleware. | |
| 48 | + | GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) | |
| 45 | 49 | } | |
| @@ -42,4 +42,8 @@ type AuthService interface { | |||||
| 42 | 42 | ||||
| 43 | // GeneratePasswordHash generates a bcrypt hash for a password. | 43 | // GeneratePasswordHash generates a bcrypt hash for a password. | ||
| 44 | GeneratePasswordHash(password string) (string, error) | 44 | GeneratePasswordHash(password string) (string, error) | ||
| 45 | + | ||||
| 46 | + | // GetAuthStatus returns authentication status from context. | |||
| 47 | + | // Extracts and validates token claims that were set by auth middleware. | |||
| 48 | + | GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) | |||
| 45 | } | 49 | } | ||
M internal/boundaries/in/mocks/mock_auth_service.go +63 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "time" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -255,6 +255,68 @@ func (_c *MockAuthService_GenerateToken_Call) RunAndReturn(run func(ctx context. | |||
| 255 | 255 | return _c | |
| 256 | 256 | } | |
| 257 | 257 | ||
| 258 | + | // GetAuthStatus provides a mock function for the type MockAuthService | |
| 259 | + | func (_mock *MockAuthService) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |
| 260 | + | ret := _mock.Called(ctx) | |
| 261 | + | ||
| 262 | + | if len(ret) == 0 { | |
| 263 | + | panic("no return value specified for GetAuthStatus") | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | var r0 *domain.AuthStatus | |
| 267 | + | var r1 error | |
| 268 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) (*domain.AuthStatus, error)); ok { | |
| 269 | + | return returnFunc(ctx) | |
| 270 | + | } | |
| 271 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) *domain.AuthStatus); ok { | |
| 272 | + | r0 = returnFunc(ctx) | |
| 273 | + | } else { | |
| 274 | + | if ret.Get(0) != nil { | |
| 275 | + | r0 = ret.Get(0).(*domain.AuthStatus) | |
| 276 | + | } | |
| 277 | + | } | |
| 278 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 279 | + | r1 = returnFunc(ctx) | |
| 280 | + | } else { | |
| 281 | + | r1 = ret.Error(1) | |
| 282 | + | } | |
| 283 | + | return r0, r1 | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // MockAuthService_GetAuthStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthStatus' | |
| 287 | + | type MockAuthService_GetAuthStatus_Call struct { | |
| 288 | + | *mock.Call | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // GetAuthStatus is a helper method to define mock.On call | |
| 292 | + | // - ctx context.Context | |
| 293 | + | func (_e *MockAuthService_Expecter) GetAuthStatus(ctx interface{}) *MockAuthService_GetAuthStatus_Call { | |
| 294 | + | return &MockAuthService_GetAuthStatus_Call{Call: _e.mock.On("GetAuthStatus", ctx)} | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | func (_c *MockAuthService_GetAuthStatus_Call) Run(run func(ctx context.Context)) *MockAuthService_GetAuthStatus_Call { | |
| 298 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 299 | + | var arg0 context.Context | |
| 300 | + | if args[0] != nil { | |
| 301 | + | arg0 = args[0].(context.Context) | |
| 302 | + | } | |
| 303 | + | run( | |
| 304 | + | arg0, | |
| 305 | + | ) | |
| 306 | + | }) | |
| 307 | + | return _c | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | func (_c *MockAuthService_GetAuthStatus_Call) Return(authStatus *domain.AuthStatus, err error) *MockAuthService_GetAuthStatus_Call { | |
| 311 | + | _c.Call.Return(authStatus, err) | |
| 312 | + | return _c | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | func (_c *MockAuthService_GetAuthStatus_Call) RunAndReturn(run func(ctx context.Context) (*domain.AuthStatus, error)) *MockAuthService_GetAuthStatus_Call { | |
| 316 | + | _c.Call.Return(run) | |
| 317 | + | return _c | |
| 318 | + | } | |
| 319 | + | ||
| 258 | 320 | // GetAuthType provides a mock function for the type MockAuthService | |
| 259 | 321 | func (_mock *MockAuthService) GetAuthType() domain.AuthType { | |
| 260 | 322 | ret := _mock.Called() | |
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "time" | 9 | "time" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| @@ -255,6 +255,68 @@ func (_c *MockAuthService_GenerateToken_Call) RunAndReturn(run func(ctx context. | |||||
| 255 | return _c | 255 | return _c | ||
| 256 | } | 256 | } | ||
| 257 | 257 | ||||
| 258 | + | // GetAuthStatus provides a mock function for the type MockAuthService | |||
| 259 | + | func (_mock *MockAuthService) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |||
| 260 | + | ret := _mock.Called(ctx) | |||
| 261 | + | ||||
| 262 | + | if len(ret) == 0 { | |||
| 263 | + | panic("no return value specified for GetAuthStatus") | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | var r0 *domain.AuthStatus | |||
| 267 | + | var r1 error | |||
| 268 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) (*domain.AuthStatus, error)); ok { | |||
| 269 | + | return returnFunc(ctx) | |||
| 270 | + | } | |||
| 271 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) *domain.AuthStatus); ok { | |||
| 272 | + | r0 = returnFunc(ctx) | |||
| 273 | + | } else { | |||
| 274 | + | if ret.Get(0) != nil { | |||
| 275 | + | r0 = ret.Get(0).(*domain.AuthStatus) | |||
| 276 | + | } | |||
| 277 | + | } | |||
| 278 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 279 | + | r1 = returnFunc(ctx) | |||
| 280 | + | } else { | |||
| 281 | + | r1 = ret.Error(1) | |||
| 282 | + | } | |||
| 283 | + | return r0, r1 | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | // MockAuthService_GetAuthStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthStatus' | |||
| 287 | + | type MockAuthService_GetAuthStatus_Call struct { | |||
| 288 | + | *mock.Call | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // GetAuthStatus is a helper method to define mock.On call | |||
| 292 | + | // - ctx context.Context | |||
| 293 | + | func (_e *MockAuthService_Expecter) GetAuthStatus(ctx interface{}) *MockAuthService_GetAuthStatus_Call { | |||
| 294 | + | return &MockAuthService_GetAuthStatus_Call{Call: _e.mock.On("GetAuthStatus", ctx)} | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | func (_c *MockAuthService_GetAuthStatus_Call) Run(run func(ctx context.Context)) *MockAuthService_GetAuthStatus_Call { | |||
| 298 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 299 | + | var arg0 context.Context | |||
| 300 | + | if args[0] != nil { | |||
| 301 | + | arg0 = args[0].(context.Context) | |||
| 302 | + | } | |||
| 303 | + | run( | |||
| 304 | + | arg0, | |||
| 305 | + | ) | |||
| 306 | + | }) | |||
| 307 | + | return _c | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | func (_c *MockAuthService_GetAuthStatus_Call) Return(authStatus *domain.AuthStatus, err error) *MockAuthService_GetAuthStatus_Call { | |||
| 311 | + | _c.Call.Return(authStatus, err) | |||
| 312 | + | return _c | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | func (_c *MockAuthService_GetAuthStatus_Call) RunAndReturn(run func(ctx context.Context) (*domain.AuthStatus, error)) *MockAuthService_GetAuthStatus_Call { | |||
| 316 | + | _c.Call.Return(run) | |||
| 317 | + | return _c | |||
| 318 | + | } | |||
| 319 | + | ||||
| 258 | // GetAuthType provides a mock function for the type MockAuthService | 320 | // GetAuthType provides a mock function for the type MockAuthService | ||
| 259 | func (_mock *MockAuthService) GetAuthType() domain.AuthType { | 321 | func (_mock *MockAuthService) GetAuthType() domain.AuthType { | ||
| 260 | ret := _mock.Called() | 322 | ret := _mock.Called() | ||
M internal/boundaries/in/mocks/mock_config_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/in/mocks/mock_container_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/in/mocks/mock_health_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/in/mocks/mock_proxy_service.go +1 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "net/http" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "net/http" | 9 | "net/http" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
M internal/boundaries/in/mocks/mock_registry_service.go +1 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "io" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "io" | 9 | "io" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
M internal/boundaries/in/mocks/mock_secret_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/boundaries/out" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/boundaries/out" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/boundaries/out" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/boundaries/out" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/out/mocks/mock_container_runtime.go +1 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "io" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "io" | 9 | "io" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
M internal/boundaries/out/mocks/mock_domain_secret_store.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/boundaries/out" | 8 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_event_bus.go +0 -1
| @@ -7,7 +7,6 @@ package mocks | |||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | 9 | "github.com/bnema/gordon/internal/domain" | |
| 10 | - | ||
| 11 | 10 | mock "github.com/stretchr/testify/mock" | |
| 12 | 11 | ) | |
| 13 | 12 | ||
| @@ -7,7 +7,6 @@ package mocks | |||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/boundaries/out" | 8 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 9 | "github.com/bnema/gordon/internal/domain" | 9 | "github.com/bnema/gordon/internal/domain" | ||
| 10 | - | ||||
| 11 | mock "github.com/stretchr/testify/mock" | 10 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 11 | ) | ||
| 13 | 12 | ||||
M internal/boundaries/out/mocks/mock_event_handler.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/domain" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/domain" | 8 | "github.com/bnema/gordon/internal/domain" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_event_publisher.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/domain" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/domain" | 8 | "github.com/bnema/gordon/internal/domain" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_event_subscriber.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/boundaries/out" | 8 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_token_store.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/domain/auth.go +33 -0
| @@ -2,6 +2,7 @@ | |||
| 2 | 2 | package domain | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | + | "context" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "time" | |
| @@ -264,3 +265,35 @@ func AdminScopeConfig(actions ...string) string { | |||
| 264 | 265 | func AdminScopeStatus(actions ...string) string { | |
| 265 | 266 | return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ",")) | |
| 266 | 267 | } | |
| 268 | + | ||
| 269 | + | // AuthStatus represents status of an authentication session. | |
| 270 | + | type AuthStatus struct { | |
| 271 | + | Valid bool | |
| 272 | + | Subject string | |
| 273 | + | Scopes []string | |
| 274 | + | ExpiresAt int64 // Unix timestamp, 0 if never expires | |
| 275 | + | IssuedAt int64 // Unix timestamp | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | // Context keys for auth-related domain concerns. | |
| 279 | + | type authContextKey string | |
| 280 | + | ||
| 281 | + | const ( | |
| 282 | + | // TokenClaimsKey is the context key for storing TokenClaims. | |
| 283 | + | // Allows use cases and adapters to access validated token claims from context. | |
| 284 | + | TokenClaimsKey authContextKey = "token_claims" | |
| 285 | + | // ContextKeyScopes is the context key for storing token scopes. | |
| 286 | + | ContextKeyScopes authContextKey = "token_scopes" | |
| 287 | + | // ContextKeySubject is the context key for storing token subject. | |
| 288 | + | ContextKeySubject authContextKey = "token_subject" | |
| 289 | + | ) | |
| 290 | + | ||
| 291 | + | // GetTokenClaims extracts TokenClaims from context. | |
| 292 | + | // Returns nil if claims are not present or type assertion fails. | |
| 293 | + | func GetTokenClaims(ctx context.Context) *TokenClaims { | |
| 294 | + | claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims) | |
| 295 | + | if !ok { | |
| 296 | + | return nil | |
| 297 | + | } | |
| 298 | + | return claims | |
| 299 | + | } | |
| @@ -2,6 +2,7 @@ | |||||
| 2 | package domain | 2 | package domain | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | + | "context" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "strings" | 7 | "strings" | ||
| 7 | "time" | 8 | "time" | ||
| @@ -264,3 +265,35 @@ func AdminScopeConfig(actions ...string) string { | |||||
| 264 | func AdminScopeStatus(actions ...string) string { | 265 | func AdminScopeStatus(actions ...string) string { | ||
| 265 | return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ",")) | 266 | return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ",")) | ||
| 266 | } | 267 | } | ||
| 268 | + | ||||
| 269 | + | // AuthStatus represents status of an authentication session. | |||
| 270 | + | type AuthStatus struct { | |||
| 271 | + | Valid bool | |||
| 272 | + | Subject string | |||
| 273 | + | Scopes []string | |||
| 274 | + | ExpiresAt int64 // Unix timestamp, 0 if never expires | |||
| 275 | + | IssuedAt int64 // Unix timestamp | |||
| 276 | + | } | |||
| 277 | + | ||||
| 278 | + | // Context keys for auth-related domain concerns. | |||
| 279 | + | type authContextKey string | |||
| 280 | + | ||||
| 281 | + | const ( | |||
| 282 | + | // TokenClaimsKey is the context key for storing TokenClaims. | |||
| 283 | + | // Allows use cases and adapters to access validated token claims from context. | |||
| 284 | + | TokenClaimsKey authContextKey = "token_claims" | |||
| 285 | + | // ContextKeyScopes is the context key for storing token scopes. | |||
| 286 | + | ContextKeyScopes authContextKey = "token_scopes" | |||
| 287 | + | // ContextKeySubject is the context key for storing token subject. | |||
| 288 | + | ContextKeySubject authContextKey = "token_subject" | |||
| 289 | + | ) | |||
| 290 | + | ||||
| 291 | + | // GetTokenClaims extracts TokenClaims from context. | |||
| 292 | + | // Returns nil if claims are not present or type assertion fails. | |||
| 293 | + | func GetTokenClaims(ctx context.Context) *TokenClaims { | |||
| 294 | + | claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims) | |||
| 295 | + | if !ok { | |||
| 296 | + | return nil | |||
| 297 | + | } | |||
| 298 | + | return claims | |||
| 299 | + | } | |||
M internal/usecase/auth/service.go +28 -0
| @@ -449,3 +449,31 @@ func getStringClaim(claims jwt.MapClaims, key string) string { | |||
| 449 | 449 | } | |
| 450 | 450 | return "" | |
| 451 | 451 | } | |
| 452 | + | ||
| 453 | + | // GetAuthStatus returns authentication status from context. | |
| 454 | + | // Claims are already validated by AdminAuth middleware and stored in context. | |
| 455 | + | func (s *Service) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |
| 456 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 457 | + | zerowrap.FieldLayer: "usecase", | |
| 458 | + | zerowrap.FieldUseCase: "GetAuthStatus", | |
| 459 | + | }) | |
| 460 | + | ||
| 461 | + | // If auth is disabled, return valid status with no claims | |
| 462 | + | if !s.IsEnabled() { | |
| 463 | + | return &domain.AuthStatus{Valid: true}, nil | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | // Extract claims from context (set by AdminAuth middleware) | |
| 467 | + | claims := domain.GetTokenClaims(ctx) | |
| 468 | + | if claims == nil { | |
| 469 | + | return &domain.AuthStatus{Valid: false}, nil | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | return &domain.AuthStatus{ | |
| 473 | + | Valid: true, | |
| 474 | + | Subject: claims.Subject, | |
| 475 | + | Scopes: claims.Scopes, | |
| 476 | + | ExpiresAt: claims.ExpiresAt, | |
| 477 | + | IssuedAt: claims.IssuedAt, | |
| 478 | + | }, nil | |
| 479 | + | } | |
| @@ -449,3 +449,31 @@ func getStringClaim(claims jwt.MapClaims, key string) string { | |||||
| 449 | } | 449 | } | ||
| 450 | return "" | 450 | return "" | ||
| 451 | } | 451 | } | ||
| 452 | + | ||||
| 453 | + | // GetAuthStatus returns authentication status from context. | |||
| 454 | + | // Claims are already validated by AdminAuth middleware and stored in context. | |||
| 455 | + | func (s *Service) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |||
| 456 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |||
| 457 | + | zerowrap.FieldLayer: "usecase", | |||
| 458 | + | zerowrap.FieldUseCase: "GetAuthStatus", | |||
| 459 | + | }) | |||
| 460 | + | ||||
| 461 | + | // If auth is disabled, return valid status with no claims | |||
| 462 | + | if !s.IsEnabled() { | |||
| 463 | + | return &domain.AuthStatus{Valid: true}, nil | |||
| 464 | + | } | |||
| 465 | + | ||||
| 466 | + | // Extract claims from context (set by AdminAuth middleware) | |||
| 467 | + | claims := domain.GetTokenClaims(ctx) | |||
| 468 | + | if claims == nil { | |||
| 469 | + | return &domain.AuthStatus{Valid: false}, nil | |||
| 470 | + | } | |||
| 471 | + | ||||
| 472 | + | return &domain.AuthStatus{ | |||
| 473 | + | Valid: true, | |||
| 474 | + | Subject: claims.Subject, | |||
| 475 | + | Scopes: claims.Scopes, | |||
| 476 | + | ExpiresAt: claims.ExpiresAt, | |||
| 477 | + | IssuedAt: claims.IssuedAt, | |||
| 478 | + | }, nil | |||
| 479 | + | } | |||