1   /*
2    * Copyright 2019 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.metadata;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static java.util.Objects.requireNonNull;
21  
22  import java.util.Map;
23  import java.util.function.Function;
24  import java.util.stream.Collectors;
25  
26  import javax.annotation.Nullable;
27  
28  import com.fasterxml.jackson.annotation.JsonCreator;
29  import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
30  import com.fasterxml.jackson.annotation.JsonInclude;
31  import com.fasterxml.jackson.annotation.JsonInclude.Include;
32  import com.fasterxml.jackson.annotation.JsonProperty;
33  import com.google.common.base.MoreObjects;
34  import com.google.common.collect.ImmutableMap;
35  
36  import com.linecorp.centraldogma.server.internal.admin.service.TokenNotFoundException;
37  
38  /**
39   * Holds a token map and a secret map for fast lookup.
40   */
41  @JsonIgnoreProperties(ignoreUnknown = true)
42  @JsonInclude(Include.NON_NULL)
43  public final class Tokens {
44  
45      static final String SECRET_PREFIX = "appToken-";
46  
47      /**
48       * Tokens which belong to this project.
49       */
50      private final Map<String, Token> appIds;
51  
52      /**
53       * A mapping of secret and {@link Token#id()}.
54       */
55      private final Map<String, String> secrets;
56  
57      /**
58       * Creates a new empty instance.
59       */
60      public Tokens() {
61          this(ImmutableMap.of(), ImmutableMap.of());
62      }
63  
64      /**
65       * Creates a new instance with the given application IDs and secrets.
66       */
67      @JsonCreator
68      public Tokens(@JsonProperty("appIds") Map<String, Token> appIds,
69                    @JsonProperty("secrets") Map<String, String> secrets) {
70          this.appIds = requireNonNull(appIds, "appIds");
71          this.secrets = requireNonNull(secrets, "secrets");
72      }
73  
74      /**
75       * Returns the application {@link Token}s.
76       */
77      @JsonProperty
78      public Map<String, Token> appIds() {
79          return appIds;
80      }
81  
82      /**
83       * Returns the secrets.
84       */
85      @JsonProperty
86      public Map<String, String> secrets() {
87          return secrets;
88      }
89  
90      /**
91       * Returns the {@link Token} that corresponds to the specified application ID.
92       */
93      public Token get(String appId) {
94          final Token token = getOrDefault(appId, null);
95          if (token != null) {
96              return token;
97          }
98          throw new TokenNotFoundException("Application ID not found: " + appId);
99      }
100 
101     /**
102      * Returns the {@link Token} that corresponds to the specified application ID. {@code defaultValue} is
103      * returned if there's no such application.
104      */
105     @Nullable
106     public Token getOrDefault(String appId, @Nullable Token defaultValue) {
107         requireNonNull(appId, "appId");
108         final Token token = appIds.get(appId);
109         if (token != null) {
110             return token;
111         }
112         return defaultValue;
113     }
114 
115     /**
116      * Returns the {@link Token} that corresponds to the specified secret.
117      */
118     public Token findBySecret(String secret) {
119         final Token token = findBySecretOrDefault(secret, null);
120         if (token != null) {
121             return token;
122         }
123         throw new TokenNotFoundException("Secret not found: " + secret);
124     }
125 
126     /**
127      * Returns the {@link Token} that corresponds to the specified secret. {@code defaultValue} is returned
128      * if there's no such secret.
129      */
130     @Nullable
131     public Token findBySecretOrDefault(String secret, @Nullable Token defaultValue) {
132         requireNonNull(secret, "secret");
133         if (!secret.startsWith(SECRET_PREFIX)) {
134             return defaultValue;
135         }
136         final String appId = secrets.get(secret);
137         if (appId != null) {
138             return getOrDefault(appId, defaultValue);
139         }
140         return defaultValue;
141     }
142 
143     /**
144      * Returns a new {@link Tokens} which does not contain any secrets.
145      */
146     public Tokens withoutSecret() {
147         final Map<String, Token> appIds =
148                 appIds().values().stream()
149                         .map(Token::withoutSecret)
150                         .collect(Collectors.toMap(Token::id, Function.identity()));
151         return new Tokens(appIds, ImmutableMap.of());
152     }
153 
154     @Override
155     public String toString() {
156         return MoreObjects.toStringHelper(this)
157                           .add("appIds", appIds())
158                           .add("secrets", secrets())
159                           .toString();
160     }
161 
162     /**
163      * Returns {@code true} if the specified secret is valid.
164      */
165     public static boolean isValidSecret(@Nullable String secret) {
166         return secret != null && secret.startsWith(SECRET_PREFIX);
167     }
168 
169     /**
170      * Throws an {@link IllegalArgumentException} if the specified secret is not valid.
171      */
172     public static void validateSecret(String secret) {
173         checkArgument(isValidSecret(secret),
174                       "invalid secret: " + secret +
175                       " (secret must start with '" + SECRET_PREFIX + "')");
176     }
177 }