Secretpad 源码导读

密码初始化

Based on my analysis of the deploy scripts, here is how the user creation and password handling process works:

  1. User and Password Input:

    • The main script, scripts/deploy/secretpad.sh, is responsible for setting up the environment.
    • Inside this script, the account_settings function prompts the interactive user to enter a username and password.
    • It performs basic validation on the username (check_user_name) and password (check_user_passwd) complexity.
    • If a password is not provided or is invalid after a few tries, a random one is generated by the generate_password function.
  2. Passing Credentials to the Application:

    • The shell scripts do not calculate the password hash.
    • Instead, the plaintext username and password, stored in the SECRETPAD_USER_NAME and SECRETPAD_PASSWORD environment variables, are passed directly into the secretpad Docker container when it’s started. You can see this in the docker run command within the start function of secretpad.sh.
  3. Password Hashing (Inside the Java Application):

    • The password hashing itself happens within the Java application running inside the container. The application reads these environment variables on startup, calculates the hash, and then stores the result in the user_accounts table.

Here is the breakdown of how, when, and where the password hash is calculated:

  • When: The process happens when the Secret-Pad application starts up.

  • Where: The logic is located in the DbDataInit.java file at secretpad-web/src/main/java/org/secretflow/secretpad/web/init/DbDataInit.java.

  • How:

    1. The initUserAndPwd method inside the DbDataInit class is triggered on startup.
    2. It reads the username and password from configuration properties (secretpad.auth.pad_name and secretpad.auth.pad_pwd). ==These properties are populated by the SECRETPAD_USER_NAME and SECRETPAD_PASSWORD environment variables you saw in the shell scripts.==
    3. It then calculates the password hash using the Sha256Utils.hash(password) method.
    4. The resulting SHA-256 hash is stored in the passwordHash field of the AccountsDO entity, which is then saved to the user_accounts table in your database.

    In short, the application uses a SHA-256 hashing algorithm to protect the user’s password.

init script

please tell me when these init function is called. And where is this configuration? @secretpad-web/src/main/java/org/secretflow/secretpad/web/init/

The classes you listed implement either ==CommandLineRunner or ApplicationRunner==. In Spring Boot, any bean that implements these interfaces will have its run() method executed after the application context has been loaded, but before the application starts accepting requests.

  1. @Order annotation: This determines the sequence of execution.
    • @Order(Ordered.HIGHEST_PRECEDENCE): Runs first. DbDataInit has this, so it is executed before the others.
    • @Order(Ordered.LOWEST_PRECEDENCE): Runs last. Most other initializers have this, meaning they run after DbDataInit in an otherwise undetermined order.

CommandLineRunner the method itself runs every time you launch the program.

The run method is executed once every time the application starts.

Here is the sequence of events:

  1. You start the Secretpad application (e.g., by running the JAR file).
  2. Spring Boot initializes and loads all its components.
  3. As part of this startup process, Spring Boot finds the DbDataInit bean and executes its run() method.
  4. The application is now fully running.
  5. You stop the application.

— LATER —

  1. You start the Secretpad application again.

  2. Spring Boot initializes again.

  3. It finds the DbDataInit bean and executes its run() method again.

    So, the method itself runs every time you launch the program.

改密码的逻辑

You are correct. The run method in DbDataInit is executed every time the application starts, but the account is only created once.

This is due to two main reasons:

  1. CommandLineRunner Lifecycle: As a CommandLineRunner, Spring Boot guarantees that the run method is executed only one time during the application’s startup sequence. ==It doesn’t run again unless the application is restarted.==

  2. Defensive Code Logic: The initUserAndPwd method is written defensively to prevent creating duplicate users. Let’s look at the key lines:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
        // 1. Try to find a user with the configured name in the database.  
    Optional<AccountsDO> accountsDOOptional = userAccountsRepository.findByName(accountsDO.getName());
    // 2. Check if the user was found. if (accountsDOOptional.isEmpty()) {
    // 3. If the user does NOT exist, save the new account. This only happens on the very first run. userAccountsRepository.save(accountsDO);
    } else { // 4. If the user ALREADY exists, just log a message and update the password. log.info("user {} already exists update password", username);
    accountsDOOptional.get().setPasswordHash(Sha256Utils.hash(password));
    userAccountsRepository.save(accountsDOOptional.get());
    } ```
    In summary:

    * **On the first application startup**: The database is empty, `findByName` returns nothing, the `if` block is executed, and the account is created.
    * **On every subsequent startup**: `findByName` finds the account created previously, the `else` block is executed, and the existing account's password is simply updated. No new account is created.

    This pattern makes the initialization process **idempotent**—meaning it can run multiple times without changing the result beyond the initial execution.

    ---


    # 第一个api,api/v1alpha1/p2p/project/list

    When you call the `api/v1alpha1/p2p/project/list` API, the following functions are called in order:

    1. `org.secretflow.secretpad.web.controller.p2p.P2PProjectController.listP2PProject(...)`
    2. `org.secretflow.secretpad.service.impl.ProjectServiceImpl.listP2PProject(...)`

    This API call **does not modify any tables** in the database. It is a read-only operation that retrieves project information from the following tables:

    * `project_inst`
    * `project_approval_config`
    * `project`
    * `vote_request`
    * `project_node`
    * `project_graph`
    * `project_job`
    * `inst`


    ## ApiResource 注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ApiResource {

    /**

    • api code * * @return api resource code {@link org.secretflow.secretpad.common.constant.resource.ApiResourceCodeConstants}
    • / String code();
      }


*   `@Target(ElementType.METHOD)`: This means the annotation can only be applied to methods.  
*   `@Retention(RetentionPolicy.RUNTIME)`: This means the annotation is available at runtime, which allows for frameworks or custom logic to inspect it.  
*   `String code();`: This defines a single attribute named `code` that takes a `String` value. **==这里的code 不是函数,见[[Java 中的注解 annotation|Java 中的注解 annotation]]==**

In the `P2PProjectController`, you can see it applied to the API methods:  
  
```java  
@ResponseBody  
@PostMapping(value = "/list", consumes = "application/json")  
@Operation(summary = "list project", description = "list project")  
@ApiResource(code = ApiResourceCodeConstants.PRJ_LIST)  
public SecretPadResponse<List<ProjectVO>> listP2PProject() {  
    return SecretPadResponse.success(projectService.listP2PProject());}  

In this example, @ApiResource(code = ApiResourceCodeConstants.PRJ_LIST) associates the listP2PProject method with the resource code PRJ_LIST.

The values for the code attribute are defined as constants in the ApiResourceCodeConstants class. For example, PRJ_LIST is defined as:

public static final String PRJ_LIST = "PRJ_LIST";```  
  
By using this annotation, the application can implement cross-cutting concerns like checking if a user has the necessary permissions to access a specific API endpoint based on the resource code.

##  注解 与授权的关系

The `@ApiResource` annotation, in conjunction with `PRJ_LIST`, is directly related to permission control in the application. Here's how it works:  
  
**1. Annotation and Resource Code:**  
  
As we've seen, the `@ApiResource` annotation is used to mark API endpoint methods. It takes a `code` as a parameter, which is a `String` that uniquely identifies that API resource. In the case of the project list API, the code is `PRJ_LIST`.  
  
```java  
@ApiResource(code = ApiResourceCodeConstants.PRJ_LIST)  
public SecretPadResponse<List<ProjectVO>> listP2PProject() {  
    // ...}  

2. AOP-based Interception:

The application uses Aspect-Oriented Programming (AOP) to intercept calls to methods annotated with @ApiResource. This is done in the InterfaceResourceAspect class.

This aspect defines a “pointcut” that targets any method annotated with @ApiResource. When such a method is called, the check method in the aspect is executed before the actual API method.

3. Permission Checking Logic:

The check method in InterfaceResourceAspect extracts the code from the @ApiResource annotation (e.g., PRJ_LIST) and calls the apiResourceAuth.check(apiResource.code()) method.

The DefaultApiResourceAuth class implements the ApiResourceAuth interface and contains the core permission checking logic. It does the following:

  • It retrieves the current user’s information from the UserContext.

  • It checks if the user has the ALL_INTERFACE_RESOURCE permission. If so, the check passes.

  • If not, it checks if the user’s apiResources (a set of resource codes they are allowed to access) contains the code from the annotation (e.g., PRJ_LIST).

  • *4. UserContext and Permissions:**

    The UserContext holds the currently logged-in user’s details, including a set of strings representing the API resources they are permitted to access. When a user logs in, their permissions are fetched and stored in the UserContext.

    In summary, the workflow is as follows:

  1. A user makes a request to an API endpoint (e.g., /api/v1alpha1/p2p/project/list).
  2. The InterfaceResourceAspect intercepts the call to the controller method because it’s annotated with @ApiResource.
  3. The aspect extracts the resource code (PRJ_LIST) from the annotation.
  4. It calls the ApiResourceAuth service to check if the current user has permission for that resource code.
  5. The DefaultApiResourceAuth service checks the user’s permissions stored in the UserContext.
  6. If the user has the PRJ_LIST permission (or the ALL_INTERFACE_RESOURCE permission), the check passes, and the controller method is executed.
  7. If the user does not have the required permission, an exception is thrown, and the user is denied access.

注解的详细讲解

Of course. Let’s break down what’s happening. It’s a great question because the magic isn’t in the @ApiResource annotation itself, but in how the application uses it.

1. @interface is not a regular interface

First, a key clarification: In Java, @interface is used to declare an annotation, not a standard interface that a class would implement.

  • An interface defines a contract (a set of methods) that a class must implement.

  • An @interface defines a new type of annotation. The annotation itself is just metadata; it’s a marker that can be placed on code, but it doesn’t contain any logic on its own.

    So, you won’t find a class that says class MyClass implements ApiResource. That’s not how annotations work.

    2. How it’s Used: Aspect-Oriented Programming (AOP)

    The “implementation” you’re looking for is not in a class that implements @ApiResource, but in a class that reads the annotation and acts on it. This is done using a powerful programming paradigm called Aspect-Oriented Programming (AOP).

    In this project, the class InterfaceResourceAspect.java is an “aspect”. Think of it as a special module that can intercept and add behavior to existing code without modifying that code directly.

    Here is the relevant code from InterfaceResourceAspect.java:

    @Aspect  
    @Component  
    public class InterfaceResourceAspect {    // ...    @Autowired  
      private ApiResourceAuth apiResourceAuth;  
    
      @Pointcut("@annotation(org.secretflow.secretpad.common.annotation.resource.ApiResource)")  
      public void pointCut() {  
      }  
      @Around("pointCut() && args(data) && @annotation(apiResource)")  
      public Object check(ProceedingJoinPoint joinPoint, Object data, ApiResource apiResource) throws Throwable {        // ...        if (!apiResourceAuth.check(apiResource.code())) {  
              throw SecretpadException.of(AuthErrorCode.AUTH_FAILED, apiResource.code());  
          }        return joinPoint.proceed();  
      }    // ...}  
    

    3. The Permission Control Workflow

    Here is the step-by-step process that connects the annotation to the permission check:

  1. A Request Arrives: A user calls the /api/v1alpha1/p2p/project/list endpoint.

  2. AOP Interception: The Spring Framework, which powers the application, sees that the listP2PProject method is annotated with @ApiResource. Because of the @Aspect and @Pointcut configuration in InterfaceResourceAspect, it intercepts the call before the listP2PProject method’s code is executed.

  3. The check Method Runs: The @Around advice in the aspect is triggered, and its check method is executed.

  4. Metadata is Read: The check method receives the ApiResource annotation instance as a parameter. It then calls apiResource.code() to get the string value you provided (in this case, PRJ_LIST).

  5. Permission Check is Delegated: The aspect then calls apiResourceAuth.check("PRJ_LIST"). Here, apiResourceAuth is an instance of a class that implements the ApiResourceAuth interface. The actual implementation is DefaultApiResourceAuth.

  6. The Real Check Happens: The DefaultApiResourceAuth class performs the logic:

    • It gets the current user’s data from the UserContext.
    • It checks if the user’s list of permissions (stored in UserContext.apiResources) contains the string "PRJ_LIST".
  7. Execution Proceeds or Fails:

    • If the check method returns true, the aspect calls joinPoint.proceed(), which allows the original listP2PProject method to finally execute.
    • If the check method returns false, the aspect throws an AuthErrorCode.AUTH_FAILED exception, and the request is denied with a permission error. The controller method is never executed.

    So, the @ApiResource annotation is not an interface to be implemented. It is a marker whose data is read by an AOP aspect, which then triggers the actual permission-checking logic.